json-ld 3.1.1 → 3.1.6

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,6 +23,14 @@ 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
+
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
+
24
34
  class << self
25
35
  ##
26
36
  # Add preloaded context. In the block form, the context is lazy evaulated on first use.
@@ -40,242 +50,11 @@ module JSON::LD
40
50
  end
41
51
  end
42
52
 
43
- # Term Definitions specify how properties and values have to be interpreted as well as the current vocabulary mapping and the default language
44
- class TermDefinition
45
- # @return [RDF::URI] IRI map
46
- attr_accessor :id
47
-
48
- # @return [String] term name
49
- attr_accessor :term
50
-
51
- # @return [String] Type mapping
52
- attr_accessor :type_mapping
53
-
54
- # Base container mapping, without @set
55
- # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] Container mapping
56
- attr_reader :container_mapping
57
-
58
- # @return [String] Term used for nest properties
59
- attr_accessor :nest
60
-
61
- # Language mapping of term, `false` is used if there is an explicit language mapping for this term.
62
- # @return [String] Language mapping
63
- attr_accessor :language_mapping
64
-
65
- # Direction of term, `false` is used if there is explicit direction mapping mapping for this term.
66
- # @return ["ltr", "rtl"] direction_mapping
67
- attr_accessor :direction_mapping
68
-
69
- # @return [Boolean] Reverse Property
70
- attr_accessor :reverse_property
71
-
72
- # This is a simple term definition, not an expanded term definition
73
- # @return [Boolean]
74
- attr_accessor :simple
75
-
76
- # Property used for data indexing; defaults to @index
77
- # @return [Boolean]
78
- attr_accessor :index
79
-
80
- # Indicate that term may be used as a prefix
81
- attr_writer :prefix
82
-
83
- # Term-specific context
84
- # @return [Hash{String => Object}]
85
- attr_accessor :context
86
-
87
- # Term is protected.
88
- # @return [Boolean]
89
- attr_writer :protected
90
-
91
- # This is a simple term definition, not an expanded term definition
92
- # @return [Boolean] simple
93
- def simple?; simple; end
94
-
95
- # This is an appropriate term to use as the prefix of a compact IRI
96
- # @return [Boolean] simple
97
- def prefix?; @prefix; end
98
-
99
- # Create a new Term Mapping with an ID
100
- # @param [String] term
101
- # @param [String] id
102
- # @param [String] type_mapping Type mapping
103
- # @param [Set<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] container_mapping
104
- # @param [String] language_mapping
105
- # Language mapping of term, `false` is used if there is an explicit language mapping for this term
106
- # @param ["ltr", "rtl"] direction_mapping
107
- # Direction mapping of term, `false` is used if there is an explicit direction mapping for this term
108
- # @param [Boolean] reverse_property
109
- # @param [Boolean] protected
110
- # @param [String] nest term used for nest properties
111
- # @param [Boolean] simple
112
- # This is a simple term definition, not an expanded term definition
113
- # @param [Boolean] prefix
114
- # Term may be used as a prefix
115
- def initialize(term,
116
- id: nil,
117
- index: nil,
118
- type_mapping: nil,
119
- container_mapping: nil,
120
- language_mapping: nil,
121
- direction_mapping: nil,
122
- reverse_property: false,
123
- nest: nil,
124
- protected: false,
125
- simple: false,
126
- prefix: nil,
127
- context: nil)
128
- @term = term
129
- @id = id.to_s unless id.nil?
130
- @index = index.to_s unless index.nil?
131
- @type_mapping = type_mapping.to_s unless type_mapping.nil?
132
- self.container_mapping = container_mapping
133
- @language_mapping = language_mapping unless language_mapping.nil?
134
- @direction_mapping = direction_mapping unless direction_mapping.nil?
135
- @reverse_property = reverse_property
136
- @protected = protected
137
- @nest = nest unless nest.nil?
138
- @simple = simple
139
- @prefix = prefix unless prefix.nil?
140
- @context = context unless context.nil?
141
- end
142
-
143
- # Term is protected.
144
- # @return [Boolean]
145
- def protected?; !!@protected; end
146
-
147
- # Set container mapping, from an array which may include @set
148
- def container_mapping=(mapping)
149
- mapping = case mapping
150
- when Set then mapping
151
- when Array then Set.new(mapping)
152
- when String then Set[mapping]
153
- when nil then Set.new
154
- else
155
- raise "Shouldn't happen with #{mapping.inspect}"
156
- end
157
- if @as_set = mapping.include?('@set')
158
- mapping = mapping.dup
159
- mapping.delete('@set')
160
- end
161
- @container_mapping = mapping
162
- @index ||= '@index' if mapping.include?('@index')
163
- end
164
-
165
- ##
166
- # Output Hash or String definition for this definition considering @language and @vocab
167
- #
168
- # @param [Context] context
169
- # @return [String, Hash{String => Array[String], String}]
170
- def to_context_definition(context)
171
- cid = if context.vocab && id.start_with?(context.vocab)
172
- # Nothing to return unless it's the same as the vocab
173
- id == context.vocab ? context.vocab : id.to_s[context.vocab.length..-1]
174
- else
175
- # Find a term to act as a prefix
176
- iri, prefix = context.iri_to_term.detect {|i,p| id.to_s.start_with?(i.to_s)}
177
- iri && iri != id ? "#{prefix}:#{id.to_s[iri.length..-1]}" : id
178
- end
179
-
180
- if simple?
181
- cid.to_s unless cid == term && context.vocab
182
- else
183
- defn = {}
184
- defn[reverse_property ? '@reverse' : '@id'] = cid.to_s unless cid == term && !reverse_property
185
- if type_mapping
186
- defn['@type'] = if KEYWORDS.include?(type_mapping)
187
- type_mapping
188
- else
189
- context.compact_iri(type_mapping, vocab: true)
190
- end
191
- end
192
-
193
- cm = Array(container_mapping)
194
- cm << "@set" if as_set? && !cm.include?("@set")
195
- cm = cm.first if cm.length == 1
196
- defn['@container'] = cm unless cm.empty?
197
- # Language set as false to be output as null
198
- defn['@language'] = (@language_mapping ? @language_mapping : nil) unless @language_mapping.nil?
199
- defn['@context'] = @context if @context
200
- defn['@nest'] = @nest if @nest
201
- defn['@index'] = @index if @index
202
- defn['@prefix'] = @prefix unless @prefix.nil?
203
- defn
204
- end
205
- end
206
-
207
- ##
208
- # Turn this into a source for a new instantiation
209
- # FIXME: context serialization
210
- # @return [String]
211
- def to_rb
212
- defn = [%(TermDefinition.new\(#{term.inspect})]
213
- %w(id index type_mapping container_mapping language_mapping direction_mapping reverse_property nest simple prefix context protected).each do |acc|
214
- v = instance_variable_get("@#{acc}".to_sym)
215
- v = v.to_s if v.is_a?(RDF::Term)
216
- if acc == 'container_mapping'
217
- v = v.to_a
218
- v << '@set' if as_set?
219
- v = v.first if v.length <= 1
220
- end
221
- defn << "#{acc}: #{v.inspect}" if v
222
- end
223
- defn.join(', ') + ")"
224
- end
225
-
226
- # If container mapping was defined along with @set
227
- # @return [Boolean]
228
- def as_set?; @as_set || false; end
229
-
230
- # Check if term definitions are identical, modulo @protected
231
- # @return [Boolean]
232
- def ==(other)
233
- other.is_a?(TermDefinition) &&
234
- id == other.id &&
235
- term == other.term &&
236
- type_mapping == other.type_mapping &&
237
- container_mapping == other.container_mapping &&
238
- nest == other.nest &&
239
- language_mapping == other.language_mapping &&
240
- direction_mapping == other.direction_mapping &&
241
- reverse_property == other.reverse_property &&
242
- simple == other.simple &&
243
- index == other.index &&
244
- context == other.context &&
245
- prefix? == other.prefix? &&
246
- as_set? == other.as_set?
247
- end
248
-
249
- def inspect
250
- v = %w([TD)
251
- v << "id=#{@id}"
252
- v << "index=#{index.inspect}" unless index.nil?
253
- v << "term=#{@term}"
254
- v << "rev" if reverse_property
255
- v << "container=#{container_mapping}" if container_mapping
256
- v << "as_set=#{as_set?.inspect}"
257
- v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
258
- v << "dir=#{direction_mapping.inspect}" unless direction_mapping.nil?
259
- v << "type=#{type_mapping}" unless type_mapping.nil?
260
- v << "nest=#{nest.inspect}" unless nest.nil?
261
- v << "simple=true" if @simple
262
- v << "protected=true" if @protected
263
- v << "prefix=#{@prefix.inspect}" unless @prefix.nil?
264
- v << "has-context" unless context.nil?
265
- v.join(" ") + "]"
266
- end
267
- end
268
-
269
53
  # The base.
270
54
  #
271
55
  # @return [RDF::URI] Current base IRI, used for expanding relative IRIs.
272
56
  attr_reader :base
273
57
 
274
- # The base.
275
- #
276
- # @return [RDF::URI] Document base IRI, to initialize `base`.
277
- attr_reader :doc_base
278
-
279
58
  # @return [RDF::URI] base IRI of the context, if loaded remotely.
280
59
  attr_accessor :context_base
281
60
 
@@ -316,9 +95,6 @@ module JSON::LD
316
95
  # @return [Hash{Symbol => Object}] Global options used in generating IRIs
317
96
  attr_accessor :options
318
97
 
319
- # @return [Context] A context provided to us that we can use without re-serializing XXX
320
- attr_accessor :provided_context
321
-
322
98
  # @return [BlankNodeNamer]
323
99
  attr_accessor :namer
324
100
 
@@ -328,187 +104,117 @@ module JSON::LD
328
104
  # @see #initialize
329
105
  # @see #parse
330
106
  # @param [String, #read, Array, Hash, Context] local_context
331
- # @raise [JsonLdError]
332
- # on a remote context load error, syntax error, or a reference to a term which is not defined.
333
- # @return [Context]
334
- def self.parse(local_context, protected: false, override_protected: false, propagate: true, **options)
335
- self.new(**options).parse(local_context, protected: false, override_protected: override_protected, propagate: propagate)
336
- end
337
-
338
- ##
339
- # Create new evaluation context
340
- # @param [Hash] options
341
- # @option options [String, #to_s] :base
107
+ # @param [String, #to_s] base (nil)
342
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.
343
- # @option options [Proc] :documentLoader
344
- # 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.
345
- # @option options [Hash{Symbol => String}] :prefixes
346
- # See `RDF::Reader#initialize`
347
- # @option options [String, #to_s] :vocab
348
- # Initial value for @vocab
349
- # @option options [String, #to_s] :language
350
- # Initial value for @langauge
351
- # @yield [ec]
352
- # @yieldparam [Context]
353
- # @return [Context]
354
- def initialize(**options)
355
- if options[:base]
356
- @base = @doc_base = RDF::URI(options[:base]).dup
357
- @doc_base.canonicalize! if options[:canonicalize]
358
- end
359
- self.processingMode = options[:processingMode] if options.has_key?(:processingMode)
360
- @term_definitions = {}
361
- @iri_to_term = {
362
- RDF.to_uri.to_s => "rdf",
363
- RDF::XSD.to_uri.to_s => "xsd"
364
- }
365
- @namer = BlankNodeMapper.new("t")
366
-
367
- @options = options
368
-
369
- # Load any defined prefixes
370
- (options[:prefixes] || {}).each_pair do |k, v|
371
- next if k.nil?
372
- @iri_to_term[v.to_s] = k
373
- @term_definitions[k.to_s] = TermDefinition.new(k, id: v.to_s, simple: true, prefix: true)
374
- end
375
-
376
- self.vocab = options[:vocab] if options[:vocab]
377
- self.default_language = options[:language] if options[:language] =~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
378
- @term_definitions = options[:term_definitions] if options[:term_definitions]
379
-
380
- #log_debug("init") {"iri_to_term: #{iri_to_term.inspect}"}
381
-
382
- yield(self) if block_given?
383
- end
384
-
385
- ##
386
- # Initial context, without mappings, vocab or default language
387
- #
388
- # @return [Boolean]
389
- def empty?
390
- @term_definitions.empty? && self.vocab.nil? && self.default_language.nil?
391
- end
392
-
393
- # @param [String] value must be an absolute IRI
394
- def base=(value, **options)
395
- if value
396
- raise JsonLdError::InvalidBaseIRI, "@base must be a string: #{value.inspect}" unless value.is_a?(String) || value.is_a?(RDF::URI)
397
- value = RDF::URI(value).dup
398
- value = @base.join(value) if @base && value.relative?
399
- @base = value
400
- @base.canonicalize! if @options[:canonicalize]
401
- raise JsonLdError::InvalidBaseIRI, "@base must be an absolute IRI: #{value.inspect}" unless @base.absolute? || !@options[:validate]
402
- @base
403
- else
404
- @base = nil
405
- end
406
-
407
- end
408
-
409
- # @param [String] value
410
- def default_language=(value, **options)
411
- @default_language = case value
412
- when String
413
- # Warn on an invalid language tag, unless :validate is true, in which case it's an error
414
- if value !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
415
- warn "@language must be valid BCP47: #{value.inspect}"
416
- end
417
- options[:lowercaseLanguage] ? value.downcase : value
418
- when nil
419
- nil
420
- else
421
- raise JsonLdError::InvalidDefaultLanguage, "@language must be a string: #{value.inspect}"
422
- end
423
- end
424
-
425
- # @param [String] value
426
- def default_direction=(value, **options)
427
- @default_direction = if value
428
- raise JsonLdError::InvalidBaseDirection, "@direction must be one or 'ltr', or 'rtl': #{value.inspect}" unless %w(ltr rtl).include?(value)
429
- value
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.
113
+ # @raise [JsonLdError]
114
+ # on a remote context load error, syntax error, or a reference to a term which is not defined.
115
+ # @return [Context]
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
430
124
  else
431
- nil
125
+ c.parse(local_context,
126
+ base: base,
127
+ override_protected: override_protected,
128
+ propagate: propagate)
432
129
  end
433
130
  end
434
131
 
435
132
  ##
436
- # Retrieve, or check processing mode.
437
- #
438
- # * With no arguments, retrieves the current set processingMode.
439
- # * 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"
440
- # * If expecting 1.1, and not set, it has the side-effect of setting mode to json-ld-1.1.
133
+ # Class-level cache used for retaining parsed remote contexts.
441
134
  #
442
- # @param [String, Number] expected (nil)
443
- # @return [String]
444
- def processingMode(expected = nil)
445
- case expected
446
- when 1.0, 'json-ld-1.0'
447
- @processingMode == 'json-ld-1.0'
448
- when 1.1, 'json-ld-1.1'
449
- @processingMode ||= 'json-ld-1.1'
450
- @processingMode == 'json-ld-1.1'
451
- when nil
452
- @processingMode
453
- else
454
- false
455
- end
135
+ # @return [RDF::Util::Cache]
136
+ # @private
137
+ def self.cache
138
+ @cache ||= RDF::Util::Cache.new(CACHE_SIZE)
456
139
  end
457
140
 
458
141
  ##
459
- # Set processing mode.
460
- #
461
- # * 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"
462
- #
463
- # 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.
464
- # If processingMode has been set, and it is not "json-ld-1.1", a "processing mode conflict" has been detecting, and processing is aborted.
142
+ # Class-level cache inverse contexts.
465
143
  #
466
- # @param [String, Number] expected
467
- # @return [String]
468
- # @raise [JsonLdError::ProcessingModeConflict]
469
- def processingMode=(value = nil, **options)
470
- value = "json-ld-1.1" if value == 1.1
471
- case value
472
- when "json-ld-1.0", "json-ld-1.1"
473
- if @processingMode && @processingMode != value
474
- raise JsonLdError::ProcessingModeConflict, "#{value} not compatible with #{@processingMode}"
475
- end
476
- @processingMode = value
477
- else
478
- raise JsonLdError::InvalidVersionValue, value.inspect
479
- end
144
+ # @return [RDF::Util::Cache]
145
+ # @private
146
+ def self.inverse_cache
147
+ @inverse_cache ||= RDF::Util::Cache.new(CACHE_SIZE)
480
148
  end
481
149
 
482
- # 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.
483
- # @param [String] value must be an absolute IRI
484
- def vocab=(value, **options)
485
- @vocab = case value
486
- when /_:/
487
- # BNode vocab is deprecated
488
- warn "[DEPRECATION] Blank Node vocabularies deprecated in JSON-LD 1.1." if @options[:validate] && processingMode("json-ld-1.1")
489
- value
490
- when String, RDF::URI
491
- if (RDF::URI(value.to_s).relative? && processingMode("json-ld-1.0"))
492
- raise JsonLdError::InvalidVocabMapping, "@vocab must be an absolute IRI in 1.0 mode: #{value.inspect}"
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
493
170
  end
494
- v = expand_iri(value.to_s, vocab: true, documentRelative: true)
495
- raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}" if !v.valid? && @options[:validate]
496
- v
497
- when nil
498
- nil
499
171
  else
500
- raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}"
172
+ # Don't try to cache
173
+ context = JSON::LD::Context.allocate
174
+ context.send(:initialize, **options)
175
+ context
501
176
  end
502
177
  end
503
178
 
504
- # Set propagation
505
- # @note: by the time this is called, the work has already been done.
506
- #
507
- # @param [Boolean] value
508
- def propagate=(value, **options)
509
- raise JsonLdError::InvalidContextEntry, "@propagate may only be set in 1.1 mode" if processingMode("json-ld-1.0")
510
- raise JsonLdError::InvalidPropagateValue, "@propagate must be boolean valued: #{value.inspect}" unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
511
- value
179
+ ##
180
+ # Create new evaluation context
181
+ # @param [Hash] options
182
+ # @option options [Hash{Symbol => String}] :prefixes
183
+ # See `RDF::Reader#initialize`
184
+ # @option options [String, #to_s] :vocab
185
+ # Initial value for @vocab
186
+ # @option options [String, #to_s] :language
187
+ # Initial value for @langauge
188
+ # @yield [ec]
189
+ # @yieldparam [Context]
190
+ # @return [Context]
191
+ def initialize(**options)
192
+ if options[:processingMode] == 'json-ld-1.0'
193
+ @processingMode = 'json-ld-1.0'
194
+ end
195
+ @term_definitions = {}
196
+ @iri_to_term = {
197
+ RDF.to_uri.to_s => "rdf",
198
+ RDF::XSD.to_uri.to_s => "xsd"
199
+ }
200
+ @namer = BlankNodeMapper.new("t")
201
+
202
+ @options = options
203
+
204
+ # Load any defined prefixes
205
+ (options[:prefixes] || {}).each_pair do |k, v|
206
+ next if k.nil?
207
+ @iri_to_term[v.to_s] = k
208
+ @term_definitions[k.to_s] = TermDefinition.new(k, id: v.to_s, simple: true, prefix: true)
209
+ end
210
+
211
+ self.vocab = options[:vocab] if options[:vocab]
212
+ self.default_language = options[:language] if options[:language] =~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
213
+ @term_definitions = options[:term_definitions] if options[:term_definitions]
214
+
215
+ #log_debug("init") {"iri_to_term: #{iri_to_term.inspect}"}
216
+
217
+ yield(self) if block_given?
512
218
  end
513
219
 
514
220
  # Create an Evaluation Context
@@ -521,18 +227,26 @@ module JSON::LD
521
227
  #
522
228
  #
523
229
  # @param [String, #read, Array, Hash, Context] local_context
524
- # @param [Array<String>] remote_contexts
525
- # @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.
526
232
  # @param [Boolean] override_protected Protected terms may be cleared.
527
- # @param [Boolean] propagate
233
+ # @param [Boolean] propagate (true)
528
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.
529
239
  # @raise [JsonLdError]
530
240
  # on a remote context load error, syntax error, or a reference to a term which is not defined.
531
241
  # @return [Context]
532
242
  # @see https://www.w3.org/TR/json-ld11-api/index.html#context-processing-algorithm
533
- 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)
534
249
  result = self.dup
535
- result.provided_context = local_context if self.empty?
536
250
  # Early check for @propagate, which can only appear in a local context
537
251
  propagate = local_context.is_a?(Hash) ? local_context.fetch('@propagate', propagate) : propagate
538
252
  result.previous_context ||= result.dup unless propagate
@@ -541,7 +255,7 @@ module JSON::LD
541
255
 
542
256
  local_context.each do |context|
543
257
  case context
544
- when nil
258
+ when nil,false
545
259
  # 3.1 If the `override_protected` is false, and the active context contains protected terms, an error is raised.
546
260
  if override_protected || result.term_definitions.values.none?(&:protected?)
547
261
  null_context = Context.new(**options)
@@ -553,37 +267,34 @@ module JSON::LD
553
267
  end
554
268
  when Context
555
269
  #log_debug("parse") {"context: #{context.inspect}"}
556
- result = context.dup
270
+ result = result.merge(context)
557
271
  when IO, StringIO
558
272
  #log_debug("parse") {"io: #{context}"}
559
273
  # Load context document, if it is an open file
560
274
  begin
561
275
  ctx = JSON.load(context)
562
276
  raise JSON::LD::JsonLdError::InvalidRemoteContext, "Context missing @context key" if @options[:validate] && ctx['@context'].nil?
563
- result = result.dup.parse(ctx["@context"] ? ctx["@context"].dup : {})
564
- result.provided_context = ctx["@context"] if [context] == local_context
565
- result
277
+ result = result.parse(ctx["@context"] ? ctx["@context"] : {})
566
278
  rescue JSON::ParserError => e
567
279
  #log_debug("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"}
568
280
  raise JSON::LD::JsonLdError::InvalidRemoteContext, "Failed to parse remote context at #{context}: #{e.message}" if @options[:validate]
569
- self.dup
281
+ self
570
282
  end
571
283
  when String, RDF::URI
572
284
  #log_debug("parse") {"remote: #{context}, base: #{result.context_base || result.base}"}
573
285
 
574
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].
575
- context = RDF::URI(result.context_base || options[:base]).join(context)
287
+ context = RDF::URI(result.context_base || base).join(context)
576
288
  context_canon = context.canonicalize
577
- 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)
578
293
 
579
294
  remote_contexts << context.to_s
580
295
  raise JsonLdError::ContextOverflow, "#{context}" if remote_contexts.length >= MAX_CONTEXTS_LOADED
581
296
 
582
- context_no_base = result.dup
583
- context_no_base.base = nil
584
- context_no_base.context_base = context.to_s
585
-
586
- if PRELOADED[context_canon.to_s]
297
+ cached_context = if PRELOADED[context_canon.to_s]
587
298
  # If we have a cached context, merge it into the current context (result) and use as the new context
588
299
  #log_debug("parse") {"=> cached_context: #{context_canon.to_s.inspect}"}
589
300
 
@@ -592,10 +303,10 @@ module JSON::LD
592
303
  #log_debug("parse") {"=> (call)"}
593
304
  PRELOADED[context_canon.to_s] = PRELOADED[context_canon.to_s].call
594
305
  end
595
- context = context_no_base.merge!(PRELOADED[context_canon.to_s])
306
+ PRELOADED[context_canon.to_s]
596
307
  else
597
308
  # Load context document, if it is a string
598
- begin
309
+ Context.cache[context_canon.to_s] ||= begin
599
310
  context_opts = @options.merge(
600
311
  profile: 'http://www.w3.org/ns/json-ld#context',
601
312
  requestProfile: 'http://www.w3.org/ns/json-ld#context',
@@ -604,26 +315,30 @@ module JSON::LD
604
315
  JSON::LD::API.loadRemoteDocument(context.to_s, **context_opts) do |remote_doc|
605
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.
606
317
  raise JsonLdError::InvalidRemoteContext, "#{context}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
607
- 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
608
325
  end
609
326
  rescue JsonLdError::LoadingDocumentFailed => e
610
327
  #log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"}
611
- raise JsonLdError::LoadingRemoteContextFailed, "#{context_no_base.context_base}: #{e.message}", e.backtrace
328
+ raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
612
329
  rescue JsonLdError
613
330
  raise
614
331
  rescue StandardError => e
615
332
  #log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"}
616
- raise JsonLdError::LoadingRemoteContextFailed, "#{context_no_base.context_base}: #{e.message}", e.backtrace
333
+ raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
617
334
  end
618
-
619
- # 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.
620
- context = context_no_base.parse(context, remote_contexts: remote_contexts.dup, protected: protected, override_protected: override_protected, propagate: propagate)
621
- PRELOADED[context_canon.to_s] = context.dup
622
- context.provided_context = result.provided_context
623
335
  end
624
- 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
625
341
  result = context
626
- #log_debug("parse") {"=> provided_context: #{context.inspect}"}
627
342
  when Hash
628
343
  context = context.dup # keep from modifying a hash passed as a param
629
344
 
@@ -642,31 +357,33 @@ module JSON::LD
642
357
  # Retrieve remote context and merge the remaining context object into the result.
643
358
  raise JsonLdError::InvalidContextEntry, "@import may only be used in 1.1 mode}" if result.processingMode("json-ld-1.0")
644
359
  raise JsonLdError::InvalidImportValue, "@import must be a string: #{context['@import'].inspect}" unless context['@import'].is_a?(String)
645
- source = RDF::URI(result.context_base || result.base).join(context['@import'])
360
+ import_loc = RDF::URI(result.context_base || base).join(context['@import'])
646
361
  begin
647
362
  context_opts = @options.merge(
648
363
  profile: 'http://www.w3.org/ns/json-ld#context',
649
364
  requestProfile: 'http://www.w3.org/ns/json-ld#context',
650
365
  base: nil)
651
366
  context_opts.delete(:headers)
652
- JSON::LD::API.loadRemoteDocument(source, **context_opts) do |remote_doc|
653
- # 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.
654
- 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')
655
371
  import_context = remote_doc.document['@context']
372
+ import_context.delete('@base')
656
373
  raise JsonLdError::InvalidRemoteContext, "#{import_context.to_json} must be an object" unless import_context.is_a?(Hash)
657
374
  raise JsonLdError::InvalidContextEntry, "#{import_context.to_json} must not include @import entry" if import_context.has_key?('@import')
658
375
  context.delete(key)
659
376
  context = import_context.merge(context)
660
377
  end
661
378
  rescue JsonLdError::LoadingDocumentFailed => e
662
- raise JsonLdError::LoadingRemoteContextFailed, "#{source}: #{e.message}", e.backtrace
379
+ raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
663
380
  rescue JsonLdError
664
381
  raise
665
382
  rescue StandardError => e
666
- raise JsonLdError::LoadingRemoteContextFailed, "#{source}: #{e.message}", e.backtrace
383
+ raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
667
384
  end
668
385
  else
669
- result.send(setter, context[key], remote_contexts: remote_contexts, protected: context.fetch('@protected', protected))
386
+ result.send(setter, context[key], remote_contexts: remote_contexts)
670
387
  end
671
388
  context.delete(key)
672
389
  end
@@ -677,8 +394,12 @@ module JSON::LD
677
394
  context.each_key do |key|
678
395
  # ... where key is not @base, @vocab, @language, or @version
679
396
  result.create_term_definition(context, key, defined,
397
+ base: base,
680
398
  override_protected: override_protected,
681
- 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)
682
403
  end
683
404
  else
684
405
  # 3.3) If context is not a JSON object, an invalid local context error has been detected and processing is aborted.
@@ -692,28 +413,29 @@ module JSON::LD
692
413
  # Merge in a context, creating a new context with updates from `context`
693
414
  #
694
415
  # @param [Context] context
416
+ # @param [Boolean] override_protected Allow or disallow protected terms to be changed
695
417
  # @return [Context]
696
- def merge(context)
697
- c = self.dup.merge!(context)
698
- c.instance_variable_set(:@term_definitions, context.term_definitions.dup)
699
- c
700
- 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
701
433
 
702
- ##
703
- # Update context with definitions from `context`
704
- #
705
- # @param [Context] context
706
- # @return [self]
707
- def merge!(context)
708
- # FIXME: if new context removes the default language, this won't do anything
709
- self.default_language = context.default_language if context.default_language
710
- self.vocab = context.vocab if context.vocab
711
- self.base = context.base if context.base
712
-
713
- # Merge in Term Definitions
714
- term_definitions.merge!(context.term_definitions)
715
- @inverse_context = nil # Re-build after term definitions set
716
- self
434
+ # Add term definitions
435
+ context.term_definitions.each do |term, definition|
436
+ ctx.term_definitions[term] = definition
437
+ end
438
+ ctx
717
439
  end
718
440
 
719
441
  # The following constants are used to reduce object allocations in #create_term_definition below
@@ -736,14 +458,22 @@ module JSON::LD
736
458
  # @param [Hash] local_context
737
459
  # @param [String] term
738
460
  # @param [Hash] defined
461
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
739
462
  # @param [Boolean] protected if true, causes all terms to be marked protected
740
463
  # @param [Boolean] override_protected Protected terms may be cleared.
741
- # @param [Boolean] propagate
742
- # 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.
743
468
  # @raise [JsonLdError]
744
469
  # Represents a cyclical term dependency
745
470
  # @see https://www.w3.org/TR/json-ld11-api/index.html#create-term-definition
746
- 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)
747
477
  # Expand a string value, unless it matches a keyword
748
478
  #log_debug("create_term_definition") {"term = #{term.inspect}"}
749
479
 
@@ -763,6 +493,7 @@ module JSON::LD
763
493
  # Since keywords cannot be overridden, term must not be a keyword. Otherwise, an invalid value has been detected, which is an error.
764
494
  if term == '@type' &&
765
495
  value.is_a?(Hash) &&
496
+ !value.empty? &&
766
497
  processingMode("json-ld-1.1") &&
767
498
  (value.keys - %w(@container @protected)).empty? &&
768
499
  value.fetch('@container', '@set') == '@set'
@@ -841,6 +572,11 @@ module JSON::LD
841
572
  raise JsonLdError::InvalidIRIMapping, "expected value of @reverse to be a string: #{value['@reverse'].inspect} on term #{term.inspect}" unless
842
573
  value['@reverse'].is_a?(String)
843
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
+
844
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.
845
581
  definition.id = expand_iri(value['@reverse'],
846
582
  vocab: true,
@@ -849,11 +585,6 @@ module JSON::LD
849
585
  raise JsonLdError::InvalidIRIMapping, "non-absolute @reverse IRI: #{definition.id} on term #{term.inspect}" unless
850
586
  definition.id.is_a?(RDF::Node) || definition.id.is_a?(RDF::URI) && definition.id.absolute?
851
587
 
852
- if value['@reverse'].to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
853
- warn "Values beginning with '@' are reserved for future use and ignored: #{value['@reverse']}."
854
- return
855
- end
856
-
857
588
  if term[1..-1].to_s.include?(':') && (term_iri = expand_iri(term)) != definition.id
858
589
  raise JsonLdError::InvalidIRIMapping, "term #{term} expands to #{definition.id}, not #{term_iri}"
859
590
  end
@@ -957,9 +688,17 @@ module JSON::LD
957
688
 
958
689
  if value.has_key?('@context')
959
690
  begin
960
- 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)
961
696
  # Record null context in array form
962
- 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
963
702
  rescue JsonLdError => e
964
703
  raise JsonLdError::InvalidScopedContext, "Term definition for #{term.inspect} contains illegal value for @context: #{e.message}"
965
704
  end
@@ -1017,9 +756,130 @@ module JSON::LD
1017
756
 
1018
757
  term_definitions[term] = definition
1019
758
  defined[term] = true
1020
- ensure
1021
- # Re-build after term definitions set
1022
- @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
1023
883
  end
1024
884
 
1025
885
  ##
@@ -1028,40 +888,44 @@ module JSON::LD
1028
888
  # If a context was supplied in global options, use that, otherwise, generate one
1029
889
  # from this representation.
1030
890
  #
891
+ # @param [Array, Hash, Context, IO, StringIO] provided_context (nil)
892
+ # Original context to use, if available
1031
893
  # @param [Hash{Symbol => Object}] options ({})
1032
894
  # @return [Hash]
1033
- def serialize(**options)
1034
- # FIXME: not setting provided_context now
895
+ def serialize(provided_context: nil, **options)
896
+ #log_debug("serlialize: generate context")
897
+ #log_debug("") {"=> context: #{inspect}"}
1035
898
  use_context = case provided_context
1036
899
  when String, RDF::URI
1037
900
  #log_debug "serlialize: reuse context: #{provided_context.inspect}"
1038
901
  provided_context.to_s
1039
- 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
1040
907
  #log_debug "serlialize: reuse context: #{provided_context.inspect}"
1041
908
  provided_context
909
+ when IO, StringIO
910
+ provided_context.rewind
911
+ JSON.load(provided_context).fetch('@context', {})
1042
912
  else
1043
- #log_debug("serlialize: generate context")
1044
- #log_debug("") {"=> context: #{inspect}"}
1045
913
  ctx = {}
1046
- 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
1047
916
  ctx['@direction'] = default_direction.to_s if default_direction
1048
917
  ctx['@language'] = default_language.to_s if default_language
1049
918
  ctx['@vocab'] = vocab.to_s if vocab
1050
919
 
1051
920
  # Term Definitions
1052
- term_definitions.keys.each do |term|
1053
- defn = term_definitions[term].to_context_definition(self)
1054
- ctx[term] = defn if defn
921
+ term_definitions.each do |term, defn|
922
+ ctx[term] = defn.to_context_definition(self)
1055
923
  end
1056
-
1057
- #log_debug("") {"start_doc: context=#{ctx.inspect}"}
1058
924
  ctx
1059
925
  end
1060
926
 
1061
927
  # Return hash with @context, or empty
1062
- r = {}
1063
- r['@context'] = use_context unless use_context.nil? || use_context.empty?
1064
- r
928
+ use_context.nil? || use_context.empty? ? {} : {'@context' => use_context}
1065
929
  end
1066
930
 
1067
931
  ##
@@ -1228,7 +1092,7 @@ module JSON::LD
1228
1092
  term.nest
1229
1093
  else
1230
1094
  nest_term = find_definition(term.nest)
1231
- 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'
1232
1096
  term.nest
1233
1097
  end
1234
1098
  end
@@ -1289,26 +1153,27 @@ module JSON::LD
1289
1153
  #
1290
1154
  # @param [String] value
1291
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.
1292
1160
  # @param [Boolean] documentRelative (false)
1293
- # @param [Boolean] vocab (false)
1294
1161
  # @param [Hash] local_context
1295
1162
  # Used during Context Processing.
1296
- # @param [Hash] defined
1297
- # Used during Context Processing.
1298
- # @param [Boolean] as_string (false) transform RDF::Resource values to string
1163
+ # @param [Boolean] vocab (false)
1299
1164
  # @param [Hash{Symbol => Object}] options
1300
1165
  # @return [RDF::Resource, String]
1301
1166
  # IRI or String, if it's a keyword
1302
1167
  # @raise [JSON::LD::JsonLdError::InvalidIRIMapping] if the value cannot be expanded
1303
1168
  # @see https://www.w3.org/TR/json-ld11-api/#iri-expansion
1304
1169
  def expand_iri(value,
1170
+ as_string: false,
1171
+ base: nil,
1172
+ defined: nil,
1305
1173
  documentRelative: false,
1306
- vocab: false,
1307
1174
  local_context: nil,
1308
- defined: nil,
1309
- as_string: false,
1310
- **options
1311
- )
1175
+ vocab: false,
1176
+ **options)
1312
1177
  return (value && as_string ? value.to_s : value) unless value.is_a?(String)
1313
1178
 
1314
1179
  return value if KEYWORDS.include?(value)
@@ -1328,7 +1193,8 @@ module JSON::LD
1328
1193
  # If active context has a term definition for value, and the associated mapping is a keyword, return that keyword.
1329
1194
  # If vocab is true and the active context has a term definition for value, return the associated IRI mapping.
1330
1195
  if (v_td = term_definitions[value]) && (vocab || KEYWORDS.include?(v_td.id))
1331
- 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)
1332
1198
  end
1333
1199
 
1334
1200
  # If value contains a colon (:), it is either an absolute IRI or a compact IRI:
@@ -1361,18 +1227,32 @@ module JSON::LD
1361
1227
  end
1362
1228
  end
1363
1229
 
1230
+ iri = value.is_a?(RDF::URI) ? value : RDF::URI(value)
1364
1231
  result = if vocab && self.vocab
1365
1232
  # If vocab is true, and active context has a vocabulary mapping, return the result of concatenating the vocabulary mapping with value.
1366
- self.vocab + value
1367
- elsif documentRelative && (base ||= self.base)
1368
- # 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].
1369
- value = RDF::URI(value)
1370
- value.absolute? ? value : RDF::URI(base).join(value)
1371
- 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?
1372
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.
1373
1253
  raise JSON::LD::JsonLdError::InvalidIRIMapping, "not an absolute IRI: #{value}"
1374
1254
  else
1375
- RDF::URI(value)
1255
+ iri
1376
1256
  end
1377
1257
  result && as_string ? result.to_s : result
1378
1258
  end
@@ -1393,17 +1273,17 @@ module JSON::LD
1393
1273
  # Compacts an absolute IRI to the shortest matching term or compact IRI
1394
1274
  #
1395
1275
  # @param [RDF::URI] iri
1276
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1396
1277
  # @param [Object] value
1397
1278
  # Value, used to select among various maps for the same IRI
1398
- # @param [Boolean] vocab
1399
- # specifies whether the passed iri should be compacted using the active context's vocabulary mapping
1400
1279
  # @param [Boolean] reverse
1401
1280
  # specifies whether a reverse property is being compacted
1402
- # @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
1403
1283
  #
1404
1284
  # @return [String] compacted form of IRI
1405
1285
  # @see https://www.w3.org/TR/json-ld11-api/#iri-compaction
1406
- def compact_iri(iri, value: nil, vocab: nil, reverse: false, **options)
1286
+ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil)
1407
1287
  return if iri.nil?
1408
1288
  iri = iri.to_s
1409
1289
 
@@ -1508,7 +1388,7 @@ module JSON::LD
1508
1388
  preferred_values = []
1509
1389
  preferred_values << '@reverse' if tl_value == '@reverse'
1510
1390
  if (tl_value == '@id' || tl_value == '@reverse') && value.is_a?(Hash) && value.has_key?('@id')
1511
- t_iri = compact_iri(value['@id'], vocab: true, document_relative: true)
1391
+ t_iri = compact_iri(value['@id'], vocab: true, base: base)
1512
1392
  if (r_td = term_definitions[t_iri]) && r_td.id == value['@id']
1513
1393
  preferred_values.concat(CONTAINERS_VOCAB_ID)
1514
1394
  else
@@ -1574,7 +1454,7 @@ module JSON::LD
1574
1454
 
1575
1455
  if !vocab
1576
1456
  # transform iri to a relative IRI using the document's base IRI
1577
- iri = remove_base(iri)
1457
+ iri = remove_base(self.base || base, iri)
1578
1458
  return iri
1579
1459
  else
1580
1460
  return iri
@@ -1594,26 +1474,24 @@ module JSON::LD
1594
1474
  # Value (literal or IRI) to be expanded
1595
1475
  # @param [Boolean] useNativeTypes (false) use native representations
1596
1476
  # @param [Boolean] rdfDirection (nil) decode i18n datatype if i18n-datatype
1477
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1597
1478
  # @param [Hash{Symbol => Object}] options
1598
1479
  #
1599
1480
  # @return [Hash] Object representation of value
1600
1481
  # @raise [RDF::ReaderError] if the iri cannot be expanded
1601
1482
  # @see https://www.w3.org/TR/json-ld11-api/#value-expansion
1602
- def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, **options)
1603
- #log_debug("expand_value") {"property: #{property.inspect}, value: #{value.inspect}"}
1604
-
1483
+ def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, base: nil, **options)
1605
1484
  td = term_definitions.fetch(property, TermDefinition.new(property))
1606
1485
 
1607
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.
1608
1487
  if value.is_a?(String) && td.type_mapping == '@id'
1609
1488
  #log_debug("") {"as relative IRI: #{value.inspect}"}
1610
- return {'@id' => expand_iri(value, documentRelative: true).to_s}
1489
+ return {'@id' => expand_iri(value, documentRelative: true, base: base).to_s}
1611
1490
  end
1612
1491
 
1613
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.
1614
1493
  if value.is_a?(String) && td.type_mapping == '@vocab'
1615
- #log_debug("") {"as vocab IRI: #{value.inspect}"}
1616
- 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}
1617
1495
  end
1618
1496
 
1619
1497
  value = RDF::Literal(value) if
@@ -1623,16 +1501,14 @@ module JSON::LD
1623
1501
 
1624
1502
  result = case value
1625
1503
  when RDF::URI, RDF::Node
1626
- #log_debug("URI | BNode") { value.to_s }
1627
1504
  {'@id' => value.to_s}
1628
1505
  when RDF::Literal
1629
- #log_debug("Literal") {"datatype: #{value.datatype.inspect}"}
1630
1506
  res = {}
1631
1507
  if value.datatype == RDF::URI(RDF.to_uri + "JSON") && processingMode('json-ld-1.1')
1632
1508
  # Value parsed as JSON
1633
1509
  # FIXME: MultiJson
1634
- res['@value'] = ::JSON.parse(value.object)
1635
1510
  res['@type'] = '@json'
1511
+ res['@value'] = ::JSON.parse(value.object)
1636
1512
  elsif value.datatype.start_with?("https://www.w3.org/ns/i18n#") && rdfDirection == 'i18n-datatype' && processingMode('json-ld-1.1')
1637
1513
  lang, dir = value.datatype.fragment.split('_')
1638
1514
  res['@value'] = value.to_s
@@ -1648,24 +1524,23 @@ module JSON::LD
1648
1524
  end
1649
1525
  res['@direction'] = dir
1650
1526
  elsif useNativeTypes && RDF_LITERAL_NATIVE_TYPES.include?(value.datatype)
1651
- res['@value'] = value.object
1652
1527
  res['@type'] = uri(coerce(property)) if coerce(property)
1528
+ res['@value'] = value.object
1653
1529
  else
1654
1530
  value.canonicalize! if value.datatype == RDF::XSD.double
1655
- res['@value'] = value.to_s
1656
1531
  if coerce(property)
1657
1532
  res['@type'] = uri(coerce(property)).to_s
1658
1533
  elsif value.has_datatype?
1659
1534
  res['@type'] = uri(value.datatype).to_s
1660
1535
  elsif value.has_language? || language(property)
1661
1536
  res['@language'] = (value.language || language(property)).to_s
1662
- # FIXME: direction
1663
1537
  end
1538
+ res['@value'] = value.to_s
1664
1539
  end
1665
1540
  res
1666
1541
  else
1667
1542
  # Otherwise, initialize result to a JSON object with an @value member whose value is set to value.
1668
- res = {'@value' => value}
1543
+ res = {}
1669
1544
 
1670
1545
  if td.type_mapping && !CONTAINERS_ID_VOCAB.include?(td.type_mapping.to_s)
1671
1546
  res['@type'] = td.type_mapping.to_s
@@ -1676,10 +1551,9 @@ module JSON::LD
1676
1551
  res['@direction'] = direction if direction
1677
1552
  end
1678
1553
 
1679
- res
1554
+ res.merge('@value' => value)
1680
1555
  end
1681
1556
 
1682
- #log_debug("") {"=> #{result.inspect}"}
1683
1557
  result
1684
1558
  rescue ::JSON::ParserError => e
1685
1559
  raise JSON::LD::JsonLdError::InvalidJsonLiteral, e.message
@@ -1692,13 +1566,13 @@ module JSON::LD
1692
1566
  # Associated property used to find coercion rules
1693
1567
  # @param [Hash] value
1694
1568
  # Value (literal or IRI), in full object representation, to be compacted
1695
- # @param [Hash{Symbol => Object}] options
1569
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1696
1570
  #
1697
1571
  # @return [Hash] Object representation of value
1698
1572
  # @raise [JsonLdError] if the iri cannot be expanded
1699
1573
  # @see https://www.w3.org/TR/json-ld11-api/#value-compaction
1700
1574
  # FIXME: revisit the specification version of this.
1701
- def compact_value(property, value, **options)
1575
+ def compact_value(property, value, base: nil)
1702
1576
  #log_debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}"}
1703
1577
 
1704
1578
  indexing = index?(value) && container(property).include?('@index')
@@ -1709,7 +1583,7 @@ module JSON::LD
1709
1583
  when coerce(property) == '@id' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty?
1710
1584
  # Compact an @id coercion
1711
1585
  #log_debug("") {" (@id & coerce)"}
1712
- compact_iri(value['@id'])
1586
+ compact_iri(value['@id'], base: base)
1713
1587
  when coerce(property) == '@vocab' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty?
1714
1588
  # Compact an @id coercion
1715
1589
  #log_debug("") {" (@id & coerce & vocab)"}
@@ -1799,13 +1673,21 @@ module JSON::LD
1799
1673
  v.join(" ") + "]"
1800
1674
  end
1801
1675
 
1676
+ # Duplicate an active context, allowing it to be modified.
1802
1677
  def dup
1803
- # Also duplicate mappings, coerce and list
1804
1678
  that = self
1805
- 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
+
1806
1688
  ec.instance_eval do
1807
1689
  @term_definitions = that.term_definitions.dup
1808
- @iri_to_term = that.iri_to_term.dup
1690
+ @iri_to_term = that.iri_to_term
1809
1691
  end
1810
1692
  ec
1811
1693
  end
@@ -1819,7 +1701,7 @@ module JSON::LD
1819
1701
  # @param [String] term
1820
1702
  # @return [Boolean]
1821
1703
  def term_valid?(term)
1822
- term.is_a?(String)
1704
+ term.is_a?(String) && !term.empty?
1823
1705
  end
1824
1706
 
1825
1707
  # Reverse term mapping, typically used for finding aliases for keys.
@@ -1851,20 +1733,11 @@ module JSON::LD
1851
1733
  bnode(namer.get_sym($1))
1852
1734
  else
1853
1735
  value = RDF::URI(value)
1854
- value.validate! if @options[:validate]
1855
- value.canonicalize! if @options[:canonicalize]
1856
- value = RDF::URI.intern(value, {}) if @options[:intern]
1736
+ #value.validate! if options[:validate]
1857
1737
  value
1858
1738
  end
1859
1739
  end
1860
1740
 
1861
- # Clear the provided context, used for testing
1862
- # @return [Context] self
1863
- def clear_provided_context
1864
- @provided_context = nil
1865
- self
1866
- end
1867
-
1868
1741
  # Keep track of allocated BNodes
1869
1742
  #
1870
1743
  # Don't actually use the name provided, to prevent name alias issues.
@@ -1905,7 +1778,7 @@ module JSON::LD
1905
1778
  # @return [Hash{String => Hash{String => String}}]
1906
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
1907
1780
  def inverse_context
1908
- @inverse_context ||= begin
1781
+ Context.inverse_cache[self.hash] ||= begin
1909
1782
  result = {}
1910
1783
  default_language = (self.default_language || '@none').downcase
1911
1784
  term_definitions.keys.sort do |a, b|
@@ -2000,10 +1873,11 @@ module JSON::LD
2000
1873
  ##
2001
1874
  # Removes a base IRI from the given absolute IRI.
2002
1875
  #
1876
+ # @param [String] base the base used for making `iri` relative
2003
1877
  # @param [String] iri the absolute IRI
2004
1878
  # @return [String]
2005
1879
  # the relative IRI if relative to base, otherwise the absolute IRI.
2006
- def remove_base(iri)
1880
+ def remove_base(base, iri)
2007
1881
  return iri unless base
2008
1882
  @base_and_parents ||= begin
2009
1883
  u = base
@@ -2113,5 +1987,232 @@ module JSON::LD
2113
1987
  end
2114
1988
  Array(container)
2115
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
2116
2217
  end
2117
2218
  end