json-ld 3.1.3 → 3.1.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9799c8872d44570fe8213d9f9f54067ced632dcbfc593cb84f4223889f2530d
4
- data.tar.gz: 9854ae82f3e53cb08ce4e5c8f8223d8def8c3bf86cac7ab95a6abbf08a448003
3
+ metadata.gz: 4755155aaabbcf15c3f29429eb085ac5e54c0785df8a84ebb42d8dbbb840c9b3
4
+ data.tar.gz: e8f5a6fc936ee33473f8f54ae9ee1587816b3146f4c94e179eebb2ebaa85feea
5
5
  SHA512:
6
- metadata.gz: 4fb50a75c8d031026e97635ad5305d9318d455af2786b84e5ca42d0f0546ba1f1175dd5dbc05a884798145051aa352a8f1688fdb6a5ad593d057d3900d9c6f21
7
- data.tar.gz: a844a733d4f292988638dc2da6be301a78d3294b3a64533398effb0b41b802bf7f48834d5e2754d3d65a6f2f69b802bcfc0ecbc44a159fdf2389ea16ae49bbad
6
+ metadata.gz: 8ebbbad6fde10f4ce69d8485292a580d2d9dc9cf1ffafcd878efb03a3c47bdfcd2188f1f50ec3a4d96b1a3cf7718d5926118f0283204f7420765e691d5782b78
7
+ data.tar.gz: bb86cdff64bceb60c574e5a72448394c9882817990b4a734e29ab1f6bc2f177fabf2fb0437bfa5c312ca87a1f7be10123cae1bc45db8722fb1029701cb38f16c
data/README.md CHANGED
@@ -19,6 +19,12 @@ JSON::LD can now be used to create a _context_ from an RDFS/OWL definition, and
19
19
 
20
20
  Install with `gem install json-ld`
21
21
 
22
+ ### JSON-LD Streaming Profile
23
+ This gem implements an optimized streaming reader used for generating RDF from large dataset dumps formatted as JSON-LD. Such documents must correspond to the [JSON-LD Streaming Profile](https://w3c.github.io/json-ld-streaming/):
24
+
25
+ * Keys in JSON objects must be ordered with any of `@context`, and/or `@type` coming before any other keys, in that order. This includes aliases of those keys. It is strongly encouraged that `@id` be present, and come immediately after.
26
+ * JSON-LD documents can be signaled or requested in [streaming document form](https://w3c.github.io/json-ld-streaming/#dfn-streaming-document-form). The profile URI identifying the [streaming document form](https://w3c.github.io/json-ld-streaming/#dfn-streaming-document-form) is `http://www.w3.org/ns/json-ld#streaming`.
27
+
22
28
  ### MultiJson parser
23
29
  The [MultiJson](https://rubygems.org/gems/multi_json) gem is used for parsing JSON; this defaults to the native JSON parser, but will use a more performant parser if one is available. A specific parser can be specified by adding the `:adapter` option to any API call. See [MultiJson](https://rubygems.org/gems/multi_json) for more information.
24
30
 
@@ -230,7 +236,7 @@ In some cases, the built-in document loader {JSON::LD::API.documentLoader} is in
230
236
 
231
237
  All entries into the {JSON::LD::API} accept a `:documentLoader` option, which can be used to provide an alternative method to use when loading remote documents. For example:
232
238
  ```ruby
233
- def load_document_local(url, options={}, &block)
239
+ load_document_local = Proc.new do |url, **options, &block|
234
240
  if RDF::URI(url, canonicalize: true) == RDF::URI('http://schema.org/')
235
241
  remote_document = JSON::LD::API::RemoteDocument.new(url, File.read("etc/schema.org.jsonld"))
236
242
  return block_given? ? yield(remote_document) : remote_document
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.1.3
1
+ 3.1.4
@@ -34,6 +34,8 @@ module JSON
34
34
  autoload :Normalize, 'json/ld/normalize'
35
35
  autoload :Reader, 'json/ld/reader'
36
36
  autoload :Resource, 'json/ld/resource'
37
+ autoload :StreamingReader, 'json/ld/streaming_reader'
38
+ autoload :StreamingWriter, 'json/ld/streaming_writer'
37
39
  autoload :VERSION, 'json/ld/version'
38
40
  autoload :Writer, 'json/ld/writer'
39
41
 
@@ -143,6 +145,7 @@ module JSON
143
145
  class InvalidScopedContext < JsonLdError; @code = "invalid scoped context"; end
144
146
  class InvalidScriptElement < JsonLdError; @code = "invalid script element"; end
145
147
  class InvalidSetOrListObject < JsonLdError; @code = "invalid set or list object"; end
148
+ class InvalidStreamingKeyOrder < JsonLdError; @code = 'invalid streaming key order' end
146
149
  class InvalidTermDefinition < JsonLdError; @code = "invalid term definition"; end
147
150
  class InvalidBaseDirection < JsonLdError; @code = "invalid base direction"; end
148
151
  class InvalidTypedValue < JsonLdError; @code = "invalid typed value"; end
@@ -66,7 +66,7 @@ module JSON::LD
66
66
  # @param [String, #read, Hash, Array, JSON::LD::Context] context
67
67
  # An external context to use additionally to the context embedded in input when expanding the input.
68
68
  # @param [Hash{Symbol => Object}] options
69
- # @option options [String, #to_s] :base
69
+ # @option options [RDF::URI, String, #to_s] :base
70
70
  # 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. If not specified, and a base IRI is found from `input`, options[:base] will be modified with this value.
71
71
  # @option options [Boolean] :compactArrays (true)
72
72
  # If set to `true`, the JSON-LD processor replaces arrays with just one element with that element during compaction. If set to `false`, all arrays will remain arrays even if they have just one element.
@@ -108,6 +108,7 @@ module JSON::LD
108
108
  }.merge(options)
109
109
  @namer = unique_bnodes ? BlankNodeUniqer.new : (rename_bnodes ? BlankNodeNamer.new("b") : BlankNodeMapper.new)
110
110
 
111
+ @options[:base] = RDF::URI(@options[:base]) if @options[:base] && !@options[:base].is_a?(RDF::URI)
111
112
  # For context via Link header
112
113
  _, context_ref = nil, nil
113
114
 
@@ -117,7 +118,7 @@ module JSON::LD
117
118
  remote_doc = self.class.loadRemoteDocument(input, **@options)
118
119
 
119
120
  context_ref = remote_doc.contextUrl
120
- @options[:base] = remote_doc.documentUrl if remote_doc.documentUrl && !@options[:no_default_base]
121
+ @options[:base] = RDF::URI(remote_doc.documentUrl) if remote_doc.documentUrl && !@options[:no_default_base]
121
122
 
122
123
  case remote_doc.document
123
124
  when String
@@ -130,7 +131,7 @@ module JSON::LD
130
131
 
131
132
  # If not provided, first use context from document, or from a Link header
132
133
  context ||= context_ref || {}
133
- @context = Context.parse(context || {}, **@options)
134
+ @context = Context.parse(context, **@options)
134
135
 
135
136
  if block_given?
136
137
  case block.arity
@@ -163,10 +164,9 @@ module JSON::LD
163
164
  # If a block is given, the result of evaluating the block is returned, otherwise, the expanded JSON-LD document
164
165
  # @see https://www.w3.org/TR/json-ld11-api/#expansion-algorithm
165
166
  def self.expand(input, framing: false, **options, &block)
166
- result, doc_base = nil
167
+ result = doc_base = nil
167
168
  API.new(input, options[:expandContext], **options) do
168
169
  result = self.expand(self.value, nil, self.context,
169
- ordered: @options[:ordered],
170
170
  framing: framing)
171
171
  doc_base = @options[:base]
172
172
  end
@@ -218,21 +218,21 @@ module JSON::LD
218
218
  # 1) Perform the Expansion Algorithm on the JSON-LD input.
219
219
  # This removes any existing context to allow the given context to be cleanly applied.
220
220
  expanded_input = expanded ? input : API.expand(input, ordered: false, **options) do |res, base_iri|
221
- options[:base] ||= base_iri if options[:compactToRelative]
221
+ options[:base] ||= RDF::URI(base_iri) if base_iri && options[:compactToRelative]
222
222
  res
223
223
  end
224
224
 
225
225
  API.new(expanded_input, context, no_default_base: true, **options) do
226
226
  log_debug(".compact") {"expanded input: #{expanded_input.to_json(JSON_STATE) rescue 'malformed json'}"}
227
- result = compact(value, ordered: @options[:ordered])
227
+ result = compact(value)
228
228
 
229
229
  # xxx) Add the given context to the output
230
- ctx = self.context.serialize
230
+ ctx = self.context.serialize(provided_context: context)
231
231
  if result.is_a?(Array)
232
232
  kwgraph = self.context.compact_iri('@graph', vocab: true)
233
233
  result = result.empty? ? {} : {kwgraph => result}
234
234
  end
235
- result = ctx.merge(result) unless ctx.empty?
235
+ result = ctx.merge(result) unless ctx.fetch('@context', {}).empty?
236
236
  end
237
237
  block_given? ? yield(result) : result
238
238
  end
@@ -265,7 +265,7 @@ module JSON::LD
265
265
 
266
266
  # Expand input to simplify processing
267
267
  expanded_input = expanded ? input : API.expand(input, **options) do |result, base_iri|
268
- options[:base] ||= base_iri if options[:compactToRelative]
268
+ options[:base] ||= RDF::URI(base_iri) if base_iri && options[:compactToRelative]
269
269
  result
270
270
  end
271
271
 
@@ -294,9 +294,11 @@ module JSON::LD
294
294
 
295
295
  if context && !flattened.empty?
296
296
  # Otherwise, return the result of compacting flattened according the Compaction algorithm passing context ensuring that the compaction result uses the @graph keyword (or its alias) at the top-level, even if the context is empty or if there is only one element to put in the @graph array. This ensures that the returned document has a deterministic structure.
297
- compacted = as_array(compact(flattened, ordered: @options[:ordered]))
297
+ compacted = as_array(compact(flattened))
298
298
  kwgraph = self.context.compact_iri('@graph')
299
- flattened = self.context.serialize.merge(kwgraph => compacted)
299
+ flattened = self.context.
300
+ serialize(provided_context: context).
301
+ merge(kwgraph => compacted)
300
302
  end
301
303
  end
302
304
 
@@ -335,7 +337,7 @@ module JSON::LD
335
337
  def self.frame(input, frame, expanded: false, **options)
336
338
  result = nil
337
339
  options = {
338
- base: (input if input.is_a?(String)),
340
+ base: (RDF::URI(input) if input.is_a?(String)),
339
341
  compactArrays: true,
340
342
  compactToRelative: true,
341
343
  embed: '@once',
@@ -369,7 +371,7 @@ module JSON::LD
369
371
 
370
372
  # Expand input to simplify processing
371
373
  expanded_input = expanded ? input : API.expand(input, ordered: false, **options) do |res, base_iri|
372
- options[:base] ||= base_iri if options[:compactToRelative]
374
+ options[:base] ||= RDF::URI(base_iri) if base_iri && options[:compactToRelative]
373
375
  res
374
376
  end
375
377
 
@@ -426,7 +428,7 @@ module JSON::LD
426
428
  log_debug(".frame") {"expanded result: #{result.to_json(JSON_STATE) rescue 'malformed json'}"}
427
429
 
428
430
  # Compact result
429
- compacted = compact(result, ordered: @options[:ordered])
431
+ compacted = compact(result)
430
432
 
431
433
  # @replace `@null` with nil, compacting arrays
432
434
  compacted = cleanup_null(compacted)
@@ -434,11 +436,14 @@ module JSON::LD
434
436
 
435
437
  # Add the given context to the output
436
438
  result = if !compacted.is_a?(Array)
437
- context.serialize.merge(compacted)
439
+ compacted
438
440
  else
439
441
  kwgraph = context.compact_iri('@graph')
440
- context.serialize.merge({kwgraph => compacted})
442
+ {kwgraph => compacted}
441
443
  end
444
+ # Only add context if one was provided
445
+ result = context.serialize(provided_context: frame).merge(result) if frame['@context']
446
+
442
447
  log_debug(".frame") {"after compact: #{result.to_json(JSON_STATE) rescue 'malformed json'}"}
443
448
  result
444
449
  end
@@ -521,8 +526,7 @@ module JSON::LD
521
526
  API.new(nil, nil, **options) do
522
527
  result = from_statements(input,
523
528
  useRdfType: useRdfType,
524
- useNativeTypes: useNativeTypes,
525
- ordered: @options[:ordered])
529
+ useNativeTypes: useNativeTypes)
526
530
  end
527
531
 
528
532
  block_given? ? yield(result) : result
@@ -532,16 +536,18 @@ module JSON::LD
532
536
  # Uses built-in or provided documentLoader to retrieve a parsed document.
533
537
  #
534
538
  # @param [RDF::URI, String] url
539
+ # @param [String, RDF::URI] base
540
+ # Location to use as documentUrl instead of `url`.
541
+ # @option options [Proc] :documentLoader
542
+ # The callback of the loader to be used to retrieve remote documents and contexts.
535
543
  # @param [Boolean] extractAllScripts
536
544
  # If set to `true`, when extracting JSON-LD script elements from HTML, unless a specific fragment identifier is targeted, extracts all encountered JSON-LD script elements using an array form, if necessary.
537
545
  # @param [String] profile
538
546
  # When the resulting `contentType` is `text/html` or `application/xhtml+xml`, this option determines the profile to use for selecting a JSON-LD script elements.
539
547
  # @param [String] requestProfile
540
548
  # One or more IRIs to use in the request as a profile parameter.
541
- # @param [Boolean] validate
549
+ # @param [Boolean] validate (false)
542
550
  # Allow only appropriate content types
543
- # @param [String, RDF::URI] base
544
- # Location to use as documentUrl instead of `url`.
545
551
  # @param [Hash<Symbol => Object>] options
546
552
  # @yield remote_document
547
553
  # @yieldparam [RemoteDocumentRemoteDocument, RDF::Util::File::RemoteDocument] remote_document
@@ -550,13 +556,14 @@ module JSON::LD
550
556
  # If a block is given, the result of evaluating the block is returned, otherwise, the retrieved remote document and context information unless block given
551
557
  # @raise [JsonLdError]
552
558
  def self.loadRemoteDocument(url,
559
+ base: nil,
560
+ documentLoader: nil,
553
561
  extractAllScripts: false,
554
562
  profile: nil,
555
563
  requestProfile: nil,
556
564
  validate: false,
557
- base: nil,
558
565
  **options)
559
- documentLoader = options.fetch(:documentLoader, self.method(:documentLoader))
566
+ documentLoader ||= self.method(:documentLoader)
560
567
  options = OPEN_OPTS.merge(options)
561
568
  if requestProfile
562
569
  # Add any request profile
@@ -12,11 +12,14 @@ module JSON::LD
12
12
  # This algorithm compacts a JSON-LD document, such that the given context is applied. This must result in shortening any applicable IRIs to terms or compact IRIs, any applicable keywords to keyword aliases, and any applicable JSON-LD values expressed in expanded form to simple values such as strings or numbers.
13
13
  #
14
14
  # @param [Array, Hash] element
15
- # @param [String] property (nil)
16
- # @param [Boolean] ordered (true)
15
+ # @param [String, RDF::URI] base (nil)
17
16
  # Ensure output objects have keys ordered properly
17
+ # @param [String] property (nil)
18
+ # Extra validatation
18
19
  # @return [Array, Hash]
19
- def compact(element, property: nil, ordered: false)
20
+ def compact(element,
21
+ base: nil,
22
+ property: nil)
20
23
  #if property.nil?
21
24
  # log_debug("compact") {"element: #{element.inspect}, ec: #{context.inspect}"}
22
25
  #else
@@ -29,7 +32,9 @@ module JSON::LD
29
32
  case element
30
33
  when Array
31
34
  #log_debug("") {"Array #{element.inspect}"}
32
- result = element.map {|item| compact(item, property: property, ordered: ordered)}.compact
35
+ result = element.map do |item|
36
+ compact(item, base: base, property: property)
37
+ end.compact
33
38
 
34
39
  # If element has a single member and the active property has no
35
40
  # @container mapping to @list or @set, the compacted value is that
@@ -55,10 +60,13 @@ module JSON::LD
55
60
 
56
61
  # Look up term definintions from property using the original type-scoped context, if it exists, but apply them to the now current previous context
57
62
  td = input_context.term_definitions[property] if property
58
- self.context = context.parse(td.context, override_protected: true) if td && td.context
63
+ if td && !td.context.nil?
64
+ self.context = context.parse(td.context,
65
+ override_protected: true)
66
+ end
59
67
 
60
68
  if element.key?('@id') || element.key?('@value')
61
- result = context.compact_value(property, element, log_depth: @options[:log_depth])
69
+ result = context.compact_value(property, element, base: @options[:base])
62
70
  if !result.is_a?(Hash) || context.coerce(property) == '@json'
63
71
  #log_debug("") {"=> scalar result: #{result.inspect}"}
64
72
  return result
@@ -67,7 +75,8 @@ module JSON::LD
67
75
 
68
76
  # If expanded property is @list and we're contained within a list container, recursively compact this item to an array
69
77
  if list?(element) && context.container(property).include?('@list')
70
- return compact(element['@list'], property: property, ordered: ordered)
78
+ return compact(element['@list'], base: base,
79
+ property: property)
71
80
  end
72
81
 
73
82
  inside_reverse = property == '@reverse'
@@ -80,15 +89,17 @@ module JSON::LD
80
89
  sort.
81
90
  each do |term|
82
91
  term_context = input_context.term_definitions[term].context if input_context.term_definitions[term]
83
- self.context = context.parse(term_context, propagate: false) if term_context
92
+ self.context = context.parse(term_context, propagate: false) unless term_context.nil?
84
93
  end
85
94
 
86
- element.keys.opt_sort(ordered: ordered).each do |expanded_property|
95
+ element.keys.opt_sort(ordered: @options[:ordered]).each do |expanded_property|
87
96
  expanded_value = element[expanded_property]
88
97
  #log_debug("") {"#{expanded_property}: #{expanded_value.inspect}"}
89
98
 
90
99
  if expanded_property == '@id'
91
- compacted_value = Array(expanded_value).map {|expanded_id| context.compact_iri(expanded_id)}
100
+ compacted_value = Array(expanded_value).map do |expanded_id|
101
+ context.compact_iri(expanded_id, base: @options[:base])
102
+ end
92
103
 
93
104
  kw_alias = context.compact_iri('@id', vocab: true)
94
105
  as_array = compacted_value.length > 1
@@ -98,7 +109,9 @@ module JSON::LD
98
109
  end
99
110
 
100
111
  if expanded_property == '@type'
101
- compacted_value = Array(expanded_value).map {|expanded_type| input_context.compact_iri(expanded_type, vocab: true)}
112
+ compacted_value = Array(expanded_value).map do |expanded_type|
113
+ input_context.compact_iri(expanded_type, vocab: true)
114
+ end
102
115
 
103
116
  kw_alias = context.compact_iri('@type', vocab: true)
104
117
  as_array = compacted_value.length > 1 ||
@@ -110,7 +123,8 @@ module JSON::LD
110
123
  end
111
124
 
112
125
  if expanded_property == '@reverse'
113
- compacted_value = compact(expanded_value, property: '@reverse', ordered: ordered)
126
+ compacted_value = compact(expanded_value, base: base,
127
+ property: '@reverse')
114
128
  #log_debug("@reverse") {"compacted_value: #{compacted_value.inspect}"}
115
129
  # handle double-reversed properties
116
130
  compacted_value.each do |prop, value|
@@ -131,7 +145,8 @@ module JSON::LD
131
145
 
132
146
  if expanded_property == '@preserve'
133
147
  # Compact using `property`
134
- compacted_value = compact(expanded_value, property: property, ordered: ordered)
148
+ compacted_value = compact(expanded_value, base: base,
149
+ property: property)
135
150
  #log_debug("@preserve") {"compacted_value: #{compacted_value.inspect}"}
136
151
 
137
152
  unless compacted_value.is_a?(Array) && compacted_value.empty?
@@ -158,8 +173,7 @@ module JSON::LD
158
173
  context.compact_iri(expanded_property,
159
174
  value: expanded_value,
160
175
  vocab: true,
161
- reverse: inside_reverse,
162
- log_depth: @options[:log_depth])
176
+ reverse: inside_reverse)
163
177
 
164
178
  if nest_prop = context.nest(item_active_property)
165
179
  result[nest_prop] ||= {}
@@ -177,8 +191,7 @@ module JSON::LD
177
191
  context.compact_iri(expanded_property,
178
192
  value: expanded_item,
179
193
  vocab: true,
180
- reverse: inside_reverse,
181
- log_depth: @options[:log_depth])
194
+ reverse: inside_reverse)
182
195
 
183
196
 
184
197
  nest_result = if nest_prop = context.nest(item_active_property)
@@ -197,7 +210,8 @@ module JSON::LD
197
210
  else expanded_item
198
211
  end
199
212
 
200
- compacted_item = compact(value, property: item_active_property, ordered: ordered)
213
+ compacted_item = compact(value, base: base,
214
+ property: item_active_property)
201
215
  #log_debug("") {" => compacted key: #{item_active_property.inspect} for #{compacted_item.inspect}"}
202
216
 
203
217
  # handle @list
@@ -225,9 +239,9 @@ module JSON::LD
225
239
  map_object = nest_result[item_active_property] ||= {}
226
240
  # If there is no @id, create a blank node identifier to use as an index
227
241
  map_key = if container.include?('@id') && expanded_item['@id']
228
- context.compact_iri(expanded_item['@id'])
242
+ context.compact_iri(expanded_item['@id'], base: @options[:base])
229
243
  elsif container.include?('@index') && expanded_item['@index']
230
- context.compact_iri(expanded_item['@index'])
244
+ context.compact_iri(expanded_item['@index'], vocab: true)
231
245
  else
232
246
  context.compact_iri('@none', vocab: true)
233
247
  end
@@ -299,7 +313,9 @@ module JSON::LD
299
313
 
300
314
  # if compacted_item contains a single entry who's key maps to @id, then recompact the item without @type
301
315
  if compacted_item.keys.length == 1 && expanded_item.keys.include?('@id')
302
- compacted_item = compact({'@id' => expanded_item['@id']}, property: item_active_property)
316
+ compacted_item = compact({'@id' => expanded_item['@id']},
317
+ base: base,
318
+ property: item_active_property)
303
319
  end
304
320
  compacted_item
305
321
  end
@@ -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,33 @@ 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 [Proc] :documentLoader (nil)
110
+ # 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.
111
+ # @param [Boolean] override_protected (false)
112
+ # Protected terms may be cleared.
113
+ # @param [Boolean] propagate (true)
114
+ # If false, retains any previously defined term, which can be rolled back when the descending into a new node object changes.
115
+ # @param [Boolean] validate (false)
116
+ # Extra validatation
336
117
  # @raise [JsonLdError]
337
118
  # on a remote context load error, syntax error, or a reference to a term which is not defined.
338
119
  # @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)
120
+ def self.parse(local_context,
121
+ base: nil,
122
+ override_protected: false,
123
+ propagate: true,
124
+ **options)
125
+ c = self.new(**options)
126
+ if local_context.respond_to?(:empty?) && local_context.empty?
127
+ c
128
+ else
129
+ c.parse(local_context,
130
+ base: base,
131
+ override_protected: override_protected,
132
+ propagate: propagate)
133
+ end
341
134
  end
342
135
 
343
136
  ##
@@ -346,17 +139,50 @@ module JSON::LD
346
139
  # @return [RDF::Util::Cache]
347
140
  # @private
348
141
  def self.cache
349
- require 'rdf/util/cache' unless defined?(::RDF::Util::Cache)
350
142
  @cache ||= RDF::Util::Cache.new(CACHE_SIZE)
351
143
  end
352
144
 
145
+ ##
146
+ # Class-level cache inverse contexts.
147
+ #
148
+ # @return [RDF::Util::Cache]
149
+ # @private
150
+ def self.inverse_cache
151
+ @inverse_cache ||= RDF::Util::Cache.new(CACHE_SIZE)
152
+ end
153
+
154
+ ##
155
+ # @private
156
+ # Allow caching of well-known contexts
157
+ def self.new(**options)
158
+ if (options.keys - [
159
+ :compactArrays,
160
+ :documentLoader,
161
+ :extractAllScripts,
162
+ :ordered,
163
+ :processingMode,
164
+ :validate
165
+ ]).empty?
166
+ # allow caching
167
+ key = options.hash
168
+ INITIAL_CONTEXTS[key] ||= begin
169
+ context = JSON::LD::Context.allocate
170
+ context.send(:initialize, **options)
171
+ context.freeze
172
+ context.term_definitions.freeze
173
+ context
174
+ end
175
+ else
176
+ # Don't try to cache
177
+ context = JSON::LD::Context.allocate
178
+ context.send(:initialize, **options)
179
+ context
180
+ end
181
+ end
182
+
353
183
  ##
354
184
  # Create new evaluation context
355
185
  # @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
186
  # @option options [Hash{Symbol => String}] :prefixes
361
187
  # See `RDF::Reader#initialize`
362
188
  # @option options [String, #to_s] :vocab
@@ -367,11 +193,9 @@ module JSON::LD
367
193
  # @yieldparam [Context]
368
194
  # @return [Context]
369
195
  def initialize(**options)
370
- if options[:base]
371
- @base = @doc_base = RDF::URI(options[:base]).dup
372
- @doc_base.canonicalize! if options[:canonicalize]
196
+ if options[:processingMode] == 'json-ld-1.0'
197
+ @processingMode = 'json-ld-1.0'
373
198
  end
374
- self.processingMode = options[:processingMode] if options.has_key?(:processingMode)
375
199
  @term_definitions = {}
376
200
  @iri_to_term = {
377
201
  RDF.to_uri.to_s => "rdf",
@@ -397,135 +221,6 @@ module JSON::LD
397
221
  yield(self) if block_given?
398
222
  end
399
223
 
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
224
  # Create an Evaluation Context
530
225
  #
531
226
  # 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 +231,14 @@ module JSON::LD
536
231
  #
537
232
  #
538
233
  # @param [String, #read, Array, Hash, Context] local_context
539
- # @param [Array<String>] remote_contexts
234
+ # @param [String, #to_s] :base
235
+ # 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
236
  # @param [Boolean] override_protected Protected terms may be cleared.
541
- # @param [Boolean] propagate
237
+ # @param [Boolean] propagate (true)
542
238
  # If false, retains any previously defined term, which can be rolled back when the descending into a new node object changes.
239
+ # @param [Array<String>] remote_contexts ([])
240
+ # @param [Boolean] validate (false)
241
+ # Extra validatation
543
242
  # @param [Boolean] validate_scoped (true).
544
243
  # Validate scoped context, loading if necessary.
545
244
  # If false, do not load scoped contexts.
@@ -548,12 +247,12 @@ module JSON::LD
548
247
  # @return [Context]
549
248
  # @see https://www.w3.org/TR/json-ld11-api/index.html#context-processing-algorithm
550
249
  def parse(local_context,
551
- remote_contexts: [],
250
+ base: nil,
552
251
  override_protected: false,
553
252
  propagate: true,
253
+ remote_contexts: [],
554
254
  validate_scoped: true)
555
255
  result = self.dup
556
- result.provided_context = local_context if self.empty?
557
256
  # Early check for @propagate, which can only appear in a local context
558
257
  propagate = local_context.is_a?(Hash) ? local_context.fetch('@propagate', propagate) : propagate
559
258
  result.previous_context ||= result.dup unless propagate
@@ -562,7 +261,7 @@ module JSON::LD
562
261
 
563
262
  local_context.each do |context|
564
263
  case context
565
- when nil
264
+ when nil,false
566
265
  # 3.1 If the `override_protected` is false, and the active context contains protected terms, an error is raised.
567
266
  if override_protected || result.term_definitions.values.none?(&:protected?)
568
267
  null_context = Context.new(**options)
@@ -581,18 +280,17 @@ module JSON::LD
581
280
  begin
582
281
  ctx = JSON.load(context)
583
282
  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
283
+ result = result.parse(ctx["@context"] ? ctx["@context"] : {})
586
284
  rescue JSON::ParserError => e
587
285
  #log_debug("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"}
588
286
  raise JSON::LD::JsonLdError::InvalidRemoteContext, "Failed to parse remote context at #{context}: #{e.message}" if @options[:validate]
589
- self.dup
287
+ self
590
288
  end
591
289
  when String, RDF::URI
592
290
  #log_debug("parse") {"remote: #{context}, base: #{result.context_base || result.base}"}
593
291
 
594
292
  # 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)
293
+ context = RDF::URI(result.context_base || base).join(context)
596
294
  context_canon = context.canonicalize
597
295
  context_canon.scheme = 'http' if context_canon.scheme == 'https'
598
296
 
@@ -625,10 +323,10 @@ module JSON::LD
625
323
  raise JsonLdError::InvalidRemoteContext, "#{context}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
626
324
 
627
325
  # Parse stand-alone
628
- ctx = Context.new(**options)
326
+ ctx = Context.new(unfrozen: true, **options).dup
629
327
  ctx.context_base = context.to_s
630
328
  ctx = ctx.parse(remote_doc.document['@context'], remote_contexts: remote_contexts.dup)
631
- ctx.base = nil
329
+ ctx.instance_variable_set(:@base, nil)
632
330
  ctx
633
331
  end
634
332
  rescue JsonLdError::LoadingDocumentFailed => e
@@ -647,7 +345,6 @@ module JSON::LD
647
345
 
648
346
  context.previous_context = self unless propagate
649
347
  result = context
650
- #log_debug("parse") {"=> provided_context: #{context.inspect}"}
651
348
  when Hash
652
349
  context = context.dup # keep from modifying a hash passed as a param
653
350
 
@@ -666,7 +363,7 @@ module JSON::LD
666
363
  # Retrieve remote context and merge the remaining context object into the result.
667
364
  raise JsonLdError::InvalidContextEntry, "@import may only be used in 1.1 mode}" if result.processingMode("json-ld-1.0")
668
365
  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'])
366
+ import_loc = RDF::URI(result.context_base || base).join(context['@import'])
670
367
  begin
671
368
  context_opts = @options.merge(
672
369
  profile: 'http://www.w3.org/ns/json-ld#context',
@@ -674,9 +371,9 @@ module JSON::LD
674
371
  base: nil)
675
372
  context_opts.delete(:headers)
676
373
  # 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')
374
+ JSON::LD::API.loadRemoteDocument(import_loc, **context_opts) do |remote_doc|
375
+ # 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.
376
+ raise JsonLdError::InvalidRemoteContext, "#{import_loc}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
680
377
  import_context = remote_doc.document['@context']
681
378
  import_context.delete('@base')
682
379
  raise JsonLdError::InvalidRemoteContext, "#{import_context.to_json} must be an object" unless import_context.is_a?(Hash)
@@ -685,11 +382,11 @@ module JSON::LD
685
382
  context = import_context.merge(context)
686
383
  end
687
384
  rescue JsonLdError::LoadingDocumentFailed => e
688
- raise JsonLdError::LoadingRemoteContextFailed, "#{source}: #{e.message}", e.backtrace
385
+ raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
689
386
  rescue JsonLdError
690
387
  raise
691
388
  rescue StandardError => e
692
- raise JsonLdError::LoadingRemoteContextFailed, "#{source}: #{e.message}", e.backtrace
389
+ raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
693
390
  end
694
391
  else
695
392
  result.send(setter, context[key], remote_contexts: remote_contexts)
@@ -703,6 +400,7 @@ module JSON::LD
703
400
  context.each_key do |key|
704
401
  # ... where key is not @base, @vocab, @language, or @version
705
402
  result.create_term_definition(context, key, defined,
403
+ base: base,
706
404
  override_protected: override_protected,
707
405
  protected: context['@protected'],
708
406
  remote_contexts: remote_contexts.dup,
@@ -726,13 +424,12 @@ module JSON::LD
726
424
  # @param [Boolean]
727
425
  # @return [Context]
728
426
  def merge(context, override_protected: false)
729
- ctx = Context.new(term_definitions: self.term_definitions.dup(), standard_prefixes: options[:standard_prefixes])
427
+ ctx = Context.new(term_definitions: self.term_definitions, standard_prefixes: options[:standard_prefixes])
730
428
  ctx.context_base = context.context_base || self.context_base
731
429
  ctx.default_language = context.default_language || self.default_language
732
430
  ctx.default_direction = context.default_direction || self.default_direction
733
431
  ctx.vocab = context.vocab || self.vocab
734
- ctx.base = context.base || self.base
735
- ctx.provided_context = self.provided_context
432
+ ctx.base = self.base unless self.base.nil?
736
433
  if !override_protected
737
434
  ctx.term_definitions.each do |term, definition|
738
435
  next unless definition.protected? && (other = context.term_definitions[term])
@@ -769,6 +466,7 @@ module JSON::LD
769
466
  # @param [Hash] local_context
770
467
  # @param [String] term
771
468
  # @param [Hash] defined
469
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
772
470
  # @param [Boolean] protected if true, causes all terms to be marked protected
773
471
  # @param [Boolean] override_protected Protected terms may be cleared.
774
472
  # @param [Boolean] propagate
@@ -781,6 +479,7 @@ module JSON::LD
781
479
  # Represents a cyclical term dependency
782
480
  # @see https://www.w3.org/TR/json-ld11-api/index.html#create-term-definition
783
481
  def create_term_definition(local_context, term, defined,
482
+ base: nil,
784
483
  override_protected: false,
785
484
  protected: nil,
786
485
  remote_contexts: [],
@@ -1000,6 +699,7 @@ module JSON::LD
1000
699
  if value.has_key?('@context')
1001
700
  begin
1002
701
  new_ctx = self.parse(value['@context'],
702
+ base: base,
1003
703
  override_protected: true,
1004
704
  remote_contexts: remote_contexts,
1005
705
  validate_scoped: false)
@@ -1066,9 +766,130 @@ module JSON::LD
1066
766
 
1067
767
  term_definitions[term] = definition
1068
768
  defined[term] = true
1069
- ensure
1070
- # Re-build after term definitions set
1071
- @inverse_context = nil
769
+ end
770
+
771
+ ##
772
+ # Initial context, without mappings, vocab or default language
773
+ #
774
+ # @return [Boolean]
775
+ def empty?
776
+ @term_definitions.empty? && self.vocab.nil? && self.default_language.nil?
777
+ end
778
+
779
+ # @param [String] value must be an absolute IRI
780
+ def base=(value, **options)
781
+ if value
782
+ raise JsonLdError::InvalidBaseIRI, "@base must be a string: #{value.inspect}" unless value.is_a?(String) || value.is_a?(RDF::URI)
783
+ value = RDF::URI(value)
784
+ value = @base.join(value) if @base && value.relative?
785
+ # still might be relative to document
786
+ @base = value
787
+ else
788
+ @base = false
789
+ end
790
+
791
+ end
792
+
793
+ # @param [String] value
794
+ def default_language=(value, **options)
795
+ @default_language = case value
796
+ when String
797
+ # Warn on an invalid language tag, unless :validate is true, in which case it's an error
798
+ if value !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
799
+ warn "@language must be valid BCP47: #{value.inspect}"
800
+ end
801
+ options[:lowercaseLanguage] ? value.downcase : value
802
+ when nil
803
+ nil
804
+ else
805
+ raise JsonLdError::InvalidDefaultLanguage, "@language must be a string: #{value.inspect}"
806
+ end
807
+ end
808
+
809
+ # @param [String] value
810
+ def default_direction=(value, **options)
811
+ @default_direction = if value
812
+ raise JsonLdError::InvalidBaseDirection, "@direction must be one or 'ltr', or 'rtl': #{value.inspect}" unless %w(ltr rtl).include?(value)
813
+ value
814
+ else
815
+ nil
816
+ end
817
+ end
818
+
819
+ ##
820
+ # Retrieve, or check processing mode.
821
+ #
822
+ # * With no arguments, retrieves the current set processingMode.
823
+ # * 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"
824
+ # * If expecting 1.1, and not set, it has the side-effect of setting mode to json-ld-1.1.
825
+ #
826
+ # @param [String, Number] expected (nil)
827
+ # @return [String]
828
+ def processingMode(expected = nil)
829
+ case expected
830
+ when 1.0, 'json-ld-1.0'
831
+ @processingMode == 'json-ld-1.0'
832
+ when 1.1, 'json-ld-1.1'
833
+ @processingMode.nil? || @processingMode == 'json-ld-1.1'
834
+ when nil
835
+ @processingMode || 'json-ld-1.1'
836
+ else
837
+ false
838
+ end
839
+ end
840
+
841
+ ##
842
+ # Set processing mode.
843
+ #
844
+ # * 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"
845
+ #
846
+ # 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.
847
+ # If processingMode has been set, and it is not "json-ld-1.1", a "processing mode conflict" has been detecting, and processing is aborted.
848
+ #
849
+ # @param [String, Number] expected
850
+ # @return [String]
851
+ # @raise [JsonLdError::ProcessingModeConflict]
852
+ def processingMode=(value = nil, **options)
853
+ value = "json-ld-1.1" if value == 1.1
854
+ case value
855
+ when "json-ld-1.0", "json-ld-1.1"
856
+ if @processingMode && @processingMode != value
857
+ raise JsonLdError::ProcessingModeConflict, "#{value} not compatible with #{@processingMode}"
858
+ end
859
+ @processingMode = value
860
+ else
861
+ raise JsonLdError::InvalidVersionValue, value.inspect
862
+ end
863
+ end
864
+
865
+ # 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.
866
+ # @param [String] value must be an absolute IRI
867
+ def vocab=(value, **options)
868
+ @vocab = case value
869
+ when /_:/
870
+ # BNode vocab is deprecated
871
+ warn "[DEPRECATION] Blank Node vocabularies deprecated in JSON-LD 1.1." if @options[:validate] && processingMode("json-ld-1.1")
872
+ value
873
+ when String, RDF::URI
874
+ if (RDF::URI(value.to_s).relative? && processingMode("json-ld-1.0"))
875
+ raise JsonLdError::InvalidVocabMapping, "@vocab must be an absolute IRI in 1.0 mode: #{value.inspect}"
876
+ end
877
+ expand_iri(value.to_s, vocab: true, documentRelative: true)
878
+ when nil
879
+ nil
880
+ else
881
+ raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}"
882
+ end
883
+ end
884
+
885
+ # Set propagation
886
+ # @note: by the time this is called, the work has already been done.
887
+ #
888
+ # @param [Boolean] value
889
+ def propagate=(value, **options)
890
+ raise JsonLdError::InvalidContextEntry, "@propagate may only be set in 1.1 mode" if processingMode("json-ld-1.0")
891
+ raise JsonLdError::InvalidPropagateValue, "@propagate must be boolean valued: #{value.inspect}" unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
892
+ value
1072
893
  end
1073
894
 
1074
895
  ##
@@ -1077,40 +898,44 @@ module JSON::LD
1077
898
  # If a context was supplied in global options, use that, otherwise, generate one
1078
899
  # from this representation.
1079
900
  #
901
+ # @param [Array, Hash, Context, IO, StringIO] provided_context (nil)
902
+ # Original context to use, if available
1080
903
  # @param [Hash{Symbol => Object}] options ({})
1081
904
  # @return [Hash]
1082
- def serialize(**options)
1083
- # FIXME: not setting provided_context now
905
+ def serialize(provided_context: nil, **options)
906
+ #log_debug("serlialize: generate context")
907
+ #log_debug("") {"=> context: #{inspect}"}
1084
908
  use_context = case provided_context
1085
909
  when String, RDF::URI
1086
910
  #log_debug "serlialize: reuse context: #{provided_context.inspect}"
1087
911
  provided_context.to_s
1088
- when Hash, Array
912
+ when Hash
913
+ #log_debug "serlialize: reuse context: #{provided_context.inspect}"
914
+ # If it has an @context entry use it, otherwise it is assumed to be the body of a context
915
+ provided_context.fetch('@context', provided_context)
916
+ when Array
1089
917
  #log_debug "serlialize: reuse context: #{provided_context.inspect}"
1090
918
  provided_context
919
+ when IO, StringIO
920
+ provided_context.rewind
921
+ JSON.load(provided_context).fetch('@context', {})
1091
922
  else
1092
- #log_debug("serlialize: generate context")
1093
- #log_debug("") {"=> context: #{inspect}"}
1094
923
  ctx = {}
1095
- ctx['@base'] = base.to_s if base && base != doc_base
924
+ ctx['@version'] = 1.1 if @processingMode == 'json-ld-1.1'
925
+ ctx['@base'] = base.to_s if base
1096
926
  ctx['@direction'] = default_direction.to_s if default_direction
1097
927
  ctx['@language'] = default_language.to_s if default_language
1098
928
  ctx['@vocab'] = vocab.to_s if vocab
1099
929
 
1100
930
  # Term Definitions
1101
- term_definitions.keys.each do |term|
1102
- defn = term_definitions[term].to_context_definition(self)
1103
- ctx[term] = defn if defn
931
+ term_definitions.each do |term, defn|
932
+ ctx[term] = defn.to_context_definition(self)
1104
933
  end
1105
-
1106
- #log_debug("") {"start_doc: context=#{ctx.inspect}"}
1107
934
  ctx
1108
935
  end
1109
936
 
1110
937
  # Return hash with @context, or empty
1111
- r = {}
1112
- r['@context'] = use_context unless use_context.nil? || use_context.empty?
1113
- r
938
+ use_context.nil? || use_context.empty? ? {} : {'@context' => use_context}
1114
939
  end
1115
940
 
1116
941
  ##
@@ -1338,26 +1163,27 @@ module JSON::LD
1338
1163
  #
1339
1164
  # @param [String] value
1340
1165
  # A keyword, term, prefix:suffix or possibly relative IRI
1166
+ # @param [Boolean] as_string (false) transform RDF::Resource values to string
1167
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1168
+ # @param [Hash] defined
1169
+ # Used during Context Processing.
1341
1170
  # @param [Boolean] documentRelative (false)
1342
- # @param [Boolean] vocab (false)
1343
1171
  # @param [Hash] local_context
1344
1172
  # 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
1173
+ # @param [Boolean] vocab (false)
1348
1174
  # @param [Hash{Symbol => Object}] options
1349
1175
  # @return [RDF::Resource, String]
1350
1176
  # IRI or String, if it's a keyword
1351
1177
  # @raise [JSON::LD::JsonLdError::InvalidIRIMapping] if the value cannot be expanded
1352
1178
  # @see https://www.w3.org/TR/json-ld11-api/#iri-expansion
1353
1179
  def expand_iri(value,
1180
+ as_string: false,
1181
+ base: nil,
1182
+ defined: nil,
1354
1183
  documentRelative: false,
1355
- vocab: false,
1356
1184
  local_context: nil,
1357
- defined: nil,
1358
- as_string: false,
1359
- **options
1360
- )
1185
+ vocab: false,
1186
+ **options)
1361
1187
  return (value && as_string ? value.to_s : value) unless value.is_a?(String)
1362
1188
 
1363
1189
  return value if KEYWORDS.include?(value)
@@ -1377,7 +1203,8 @@ module JSON::LD
1377
1203
  # If active context has a term definition for value, and the associated mapping is a keyword, return that keyword.
1378
1204
  # If vocab is true and the active context has a term definition for value, return the associated IRI mapping.
1379
1205
  if (v_td = term_definitions[value]) && (vocab || KEYWORDS.include?(v_td.id))
1380
- return (as_string ? v_td.id.to_s : v_td.id)
1206
+ iri = base && v_td.id ? base.join(v_td.id) : v_td.id # vocab might be doc relative
1207
+ return (as_string ? iri.to_s : iri)
1381
1208
  end
1382
1209
 
1383
1210
  # If value contains a colon (:), it is either an absolute IRI or a compact IRI:
@@ -1410,18 +1237,32 @@ module JSON::LD
1410
1237
  end
1411
1238
  end
1412
1239
 
1240
+ iri = value.is_a?(RDF::URI) ? value : RDF::URI(value)
1413
1241
  result = if vocab && self.vocab
1414
1242
  # 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?
1243
+ # Note that @vocab could still be relative to a document base
1244
+ (base && self.vocab.is_a?(RDF::URI) && self.vocab.relative? ? base.join(self.vocab) : self.vocab) + value
1245
+ elsif documentRelative
1246
+ if iri.absolute?
1247
+ iri
1248
+ elsif self.base.is_a?(RDF::URI) && self.base.absolute?
1249
+ self.base.join(iri)
1250
+ elsif self.base == false
1251
+ # No resollution of `@base: null`
1252
+ iri
1253
+ elsif base && self.base
1254
+ base.join(self.base).join(iri)
1255
+ elsif base
1256
+ base.join(iri)
1257
+ else
1258
+ # Returns a relative IRI in an odd case.
1259
+ iri
1260
+ end
1261
+ elsif local_context && iri.relative?
1421
1262
  # 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
1263
  raise JSON::LD::JsonLdError::InvalidIRIMapping, "not an absolute IRI: #{value}"
1423
1264
  else
1424
- RDF::URI(value)
1265
+ iri
1425
1266
  end
1426
1267
  result && as_string ? result.to_s : result
1427
1268
  end
@@ -1442,17 +1283,17 @@ module JSON::LD
1442
1283
  # Compacts an absolute IRI to the shortest matching term or compact IRI
1443
1284
  #
1444
1285
  # @param [RDF::URI] iri
1286
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1445
1287
  # @param [Object] value
1446
1288
  # 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
1289
  # @param [Boolean] reverse
1450
1290
  # specifies whether a reverse property is being compacted
1451
- # @param [Hash{Symbol => Object}] options ({})
1291
+ # @param [Boolean] vocab
1292
+ # specifies whether the passed iri should be compacted using the active context's vocabulary mapping
1452
1293
  #
1453
1294
  # @return [String] compacted form of IRI
1454
1295
  # @see https://www.w3.org/TR/json-ld11-api/#iri-compaction
1455
- def compact_iri(iri, value: nil, vocab: nil, reverse: false, **options)
1296
+ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil)
1456
1297
  return if iri.nil?
1457
1298
  iri = iri.to_s
1458
1299
 
@@ -1557,7 +1398,7 @@ module JSON::LD
1557
1398
  preferred_values = []
1558
1399
  preferred_values << '@reverse' if tl_value == '@reverse'
1559
1400
  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)
1401
+ t_iri = compact_iri(value['@id'], vocab: true, base: base)
1561
1402
  if (r_td = term_definitions[t_iri]) && r_td.id == value['@id']
1562
1403
  preferred_values.concat(CONTAINERS_VOCAB_ID)
1563
1404
  else
@@ -1623,7 +1464,7 @@ module JSON::LD
1623
1464
 
1624
1465
  if !vocab
1625
1466
  # transform iri to a relative IRI using the document's base IRI
1626
- iri = remove_base(iri)
1467
+ iri = remove_base(self.base || base, iri)
1627
1468
  return iri
1628
1469
  else
1629
1470
  return iri
@@ -1643,26 +1484,24 @@ module JSON::LD
1643
1484
  # Value (literal or IRI) to be expanded
1644
1485
  # @param [Boolean] useNativeTypes (false) use native representations
1645
1486
  # @param [Boolean] rdfDirection (nil) decode i18n datatype if i18n-datatype
1487
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1646
1488
  # @param [Hash{Symbol => Object}] options
1647
1489
  #
1648
1490
  # @return [Hash] Object representation of value
1649
1491
  # @raise [RDF::ReaderError] if the iri cannot be expanded
1650
1492
  # @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
-
1493
+ def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, base: nil, **options)
1654
1494
  td = term_definitions.fetch(property, TermDefinition.new(property))
1655
1495
 
1656
1496
  # 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
1497
  if value.is_a?(String) && td.type_mapping == '@id'
1658
1498
  #log_debug("") {"as relative IRI: #{value.inspect}"}
1659
- return {'@id' => expand_iri(value, documentRelative: true).to_s}
1499
+ return {'@id' => expand_iri(value, documentRelative: true, base: base).to_s}
1660
1500
  end
1661
1501
 
1662
1502
  # 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
1503
  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}
1504
+ return {'@id' => expand_iri(value, vocab: true, documentRelative: true, base: base).to_s}
1666
1505
  end
1667
1506
 
1668
1507
  value = RDF::Literal(value) if
@@ -1672,16 +1511,14 @@ module JSON::LD
1672
1511
 
1673
1512
  result = case value
1674
1513
  when RDF::URI, RDF::Node
1675
- #log_debug("URI | BNode") { value.to_s }
1676
1514
  {'@id' => value.to_s}
1677
1515
  when RDF::Literal
1678
- #log_debug("Literal") {"datatype: #{value.datatype.inspect}"}
1679
1516
  res = {}
1680
1517
  if value.datatype == RDF::URI(RDF.to_uri + "JSON") && processingMode('json-ld-1.1')
1681
1518
  # Value parsed as JSON
1682
1519
  # FIXME: MultiJson
1683
- res['@value'] = ::JSON.parse(value.object)
1684
1520
  res['@type'] = '@json'
1521
+ res['@value'] = ::JSON.parse(value.object)
1685
1522
  elsif value.datatype.start_with?("https://www.w3.org/ns/i18n#") && rdfDirection == 'i18n-datatype' && processingMode('json-ld-1.1')
1686
1523
  lang, dir = value.datatype.fragment.split('_')
1687
1524
  res['@value'] = value.to_s
@@ -1697,24 +1534,23 @@ module JSON::LD
1697
1534
  end
1698
1535
  res['@direction'] = dir
1699
1536
  elsif useNativeTypes && RDF_LITERAL_NATIVE_TYPES.include?(value.datatype)
1700
- res['@value'] = value.object
1701
1537
  res['@type'] = uri(coerce(property)) if coerce(property)
1538
+ res['@value'] = value.object
1702
1539
  else
1703
1540
  value.canonicalize! if value.datatype == RDF::XSD.double
1704
- res['@value'] = value.to_s
1705
1541
  if coerce(property)
1706
1542
  res['@type'] = uri(coerce(property)).to_s
1707
1543
  elsif value.has_datatype?
1708
1544
  res['@type'] = uri(value.datatype).to_s
1709
1545
  elsif value.has_language? || language(property)
1710
1546
  res['@language'] = (value.language || language(property)).to_s
1711
- # FIXME: direction
1712
1547
  end
1548
+ res['@value'] = value.to_s
1713
1549
  end
1714
1550
  res
1715
1551
  else
1716
1552
  # Otherwise, initialize result to a JSON object with an @value member whose value is set to value.
1717
- res = {'@value' => value}
1553
+ res = {}
1718
1554
 
1719
1555
  if td.type_mapping && !CONTAINERS_ID_VOCAB.include?(td.type_mapping.to_s)
1720
1556
  res['@type'] = td.type_mapping.to_s
@@ -1725,10 +1561,9 @@ module JSON::LD
1725
1561
  res['@direction'] = direction if direction
1726
1562
  end
1727
1563
 
1728
- res
1564
+ res.merge('@value' => value)
1729
1565
  end
1730
1566
 
1731
- #log_debug("") {"=> #{result.inspect}"}
1732
1567
  result
1733
1568
  rescue ::JSON::ParserError => e
1734
1569
  raise JSON::LD::JsonLdError::InvalidJsonLiteral, e.message
@@ -1741,13 +1576,13 @@ module JSON::LD
1741
1576
  # Associated property used to find coercion rules
1742
1577
  # @param [Hash] value
1743
1578
  # Value (literal or IRI), in full object representation, to be compacted
1744
- # @param [Hash{Symbol => Object}] options
1579
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1745
1580
  #
1746
1581
  # @return [Hash] Object representation of value
1747
1582
  # @raise [JsonLdError] if the iri cannot be expanded
1748
1583
  # @see https://www.w3.org/TR/json-ld11-api/#value-compaction
1749
1584
  # FIXME: revisit the specification version of this.
1750
- def compact_value(property, value, **options)
1585
+ def compact_value(property, value, base: nil)
1751
1586
  #log_debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}"}
1752
1587
 
1753
1588
  indexing = index?(value) && container(property).include?('@index')
@@ -1758,7 +1593,7 @@ module JSON::LD
1758
1593
  when coerce(property) == '@id' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty?
1759
1594
  # Compact an @id coercion
1760
1595
  #log_debug("") {" (@id & coerce)"}
1761
- compact_iri(value['@id'])
1596
+ compact_iri(value['@id'], base: base)
1762
1597
  when coerce(property) == '@vocab' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty?
1763
1598
  # Compact an @id coercion
1764
1599
  #log_debug("") {" (@id & coerce & vocab)"}
@@ -1848,16 +1683,21 @@ module JSON::LD
1848
1683
  v.join(" ") + "]"
1849
1684
  end
1850
1685
 
1686
+ # Duplicate an active context, allowing it to be modified.
1851
1687
  def dup
1852
- # Also duplicate mappings, coerce and list
1853
1688
  that = self
1854
- ec = super
1689
+ ec = Context.new(unfrozen: true, **@options)
1690
+ ec.context_base = that.context_base
1691
+ ec.base = that.base unless that.base.nil?
1692
+ ec.default_direction = that.default_direction
1693
+ ec.default_language = that.default_language
1694
+ ec.previous_context = that.previous_context
1695
+ ec.processingMode = that.processingMode if that.instance_variable_get(:@processingModee)
1696
+ ec.vocab = that.vocab if that.vocab
1697
+
1855
1698
  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
1699
+ @term_definitions = that.term_definitions.dup
1700
+ @iri_to_term = that.iri_to_term
1861
1701
  end
1862
1702
  ec
1863
1703
  end
@@ -1903,20 +1743,11 @@ module JSON::LD
1903
1743
  bnode(namer.get_sym($1))
1904
1744
  else
1905
1745
  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]
1746
+ #value.validate! if options[:validate]
1909
1747
  value
1910
1748
  end
1911
1749
  end
1912
1750
 
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
1751
  # Keep track of allocated BNodes
1921
1752
  #
1922
1753
  # Don't actually use the name provided, to prevent name alias issues.
@@ -1957,7 +1788,7 @@ module JSON::LD
1957
1788
  # @return [Hash{String => Hash{String => String}}]
1958
1789
  # @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
1790
  def inverse_context
1960
- @inverse_context ||= begin
1791
+ Context.inverse_cache[self.hash] ||= begin
1961
1792
  result = {}
1962
1793
  default_language = (self.default_language || '@none').downcase
1963
1794
  term_definitions.keys.sort do |a, b|
@@ -2052,10 +1883,11 @@ module JSON::LD
2052
1883
  ##
2053
1884
  # Removes a base IRI from the given absolute IRI.
2054
1885
  #
1886
+ # @param [String] base the base used for making `iri` relative
2055
1887
  # @param [String] iri the absolute IRI
2056
1888
  # @return [String]
2057
1889
  # the relative IRI if relative to base, otherwise the absolute IRI.
2058
- def remove_base(iri)
1890
+ def remove_base(base, iri)
2059
1891
  return iri unless base
2060
1892
  @base_and_parents ||= begin
2061
1893
  u = base
@@ -2165,5 +1997,232 @@ module JSON::LD
2165
1997
  end
2166
1998
  Array(container)
2167
1999
  end
2000
+
2001
+ # Term Definitions specify how properties and values have to be interpreted as well as the current vocabulary mapping and the default language
2002
+ class TermDefinition
2003
+ # @return [RDF::URI] IRI map
2004
+ attr_accessor :id
2005
+
2006
+ # @return [String] term name
2007
+ attr_accessor :term
2008
+
2009
+ # @return [String] Type mapping
2010
+ attr_accessor :type_mapping
2011
+
2012
+ # Base container mapping, without @set
2013
+ # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] Container mapping
2014
+ attr_reader :container_mapping
2015
+
2016
+ # @return [String] Term used for nest properties
2017
+ attr_accessor :nest
2018
+
2019
+ # Language mapping of term, `false` is used if there is an explicit language mapping for this term.
2020
+ # @return [String] Language mapping
2021
+ attr_accessor :language_mapping
2022
+
2023
+ # Direction of term, `false` is used if there is explicit direction mapping mapping for this term.
2024
+ # @return ["ltr", "rtl"] direction_mapping
2025
+ attr_accessor :direction_mapping
2026
+
2027
+ # @return [Boolean] Reverse Property
2028
+ attr_accessor :reverse_property
2029
+
2030
+ # This is a simple term definition, not an expanded term definition
2031
+ # @return [Boolean]
2032
+ attr_accessor :simple
2033
+
2034
+ # Property used for data indexing; defaults to @index
2035
+ # @return [Boolean]
2036
+ attr_accessor :index
2037
+
2038
+ # Indicate that term may be used as a prefix
2039
+ attr_writer :prefix
2040
+
2041
+ # Term-specific context
2042
+ # @return [Hash{String => Object}]
2043
+ attr_accessor :context
2044
+
2045
+ # Term is protected.
2046
+ # @return [Boolean]
2047
+ attr_writer :protected
2048
+
2049
+ # This is a simple term definition, not an expanded term definition
2050
+ # @return [Boolean] simple
2051
+ def simple?; simple; end
2052
+
2053
+ # This is an appropriate term to use as the prefix of a compact IRI
2054
+ # @return [Boolean] simple
2055
+ def prefix?; @prefix; end
2056
+
2057
+ # Create a new Term Mapping with an ID
2058
+ # @param [String] term
2059
+ # @param [String] id
2060
+ # @param [String] type_mapping Type mapping
2061
+ # @param [Set<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] container_mapping
2062
+ # @param [String] language_mapping
2063
+ # Language mapping of term, `false` is used if there is an explicit language mapping for this term
2064
+ # @param ["ltr", "rtl"] direction_mapping
2065
+ # Direction mapping of term, `false` is used if there is an explicit direction mapping for this term
2066
+ # @param [Boolean] reverse_property
2067
+ # @param [Boolean] protected
2068
+ # @param [String] nest term used for nest properties
2069
+ # @param [Boolean] simple
2070
+ # This is a simple term definition, not an expanded term definition
2071
+ # @param [Boolean] prefix
2072
+ # Term may be used as a prefix
2073
+ def initialize(term,
2074
+ id: nil,
2075
+ index: nil,
2076
+ type_mapping: nil,
2077
+ container_mapping: nil,
2078
+ language_mapping: nil,
2079
+ direction_mapping: nil,
2080
+ reverse_property: false,
2081
+ nest: nil,
2082
+ protected: nil,
2083
+ simple: false,
2084
+ prefix: nil,
2085
+ context: nil)
2086
+ @term = term
2087
+ @id = id.to_s unless id.nil?
2088
+ @index = index.to_s unless index.nil?
2089
+ @type_mapping = type_mapping.to_s unless type_mapping.nil?
2090
+ self.container_mapping = container_mapping
2091
+ @language_mapping = language_mapping unless language_mapping.nil?
2092
+ @direction_mapping = direction_mapping unless direction_mapping.nil?
2093
+ @reverse_property = reverse_property
2094
+ @protected = protected
2095
+ @nest = nest unless nest.nil?
2096
+ @simple = simple
2097
+ @prefix = prefix unless prefix.nil?
2098
+ @context = context unless context.nil?
2099
+ end
2100
+
2101
+ # Term is protected.
2102
+ # @return [Boolean]
2103
+ def protected?; !!@protected; end
2104
+
2105
+ # Set container mapping, from an array which may include @set
2106
+ def container_mapping=(mapping)
2107
+ mapping = case mapping
2108
+ when Set then mapping
2109
+ when Array then Set.new(mapping)
2110
+ when String then Set[mapping]
2111
+ when nil then Set.new
2112
+ else
2113
+ raise "Shouldn't happen with #{mapping.inspect}"
2114
+ end
2115
+ if @as_set = mapping.include?('@set')
2116
+ mapping = mapping.dup
2117
+ mapping.delete('@set')
2118
+ end
2119
+ @container_mapping = mapping
2120
+ @index ||= '@index' if mapping.include?('@index')
2121
+ end
2122
+
2123
+ ##
2124
+ # Output Hash or String definition for this definition considering @language and @vocab
2125
+ #
2126
+ # @param [Context] context
2127
+ # @return [String, Hash{String => Array[String], String}]
2128
+ def to_context_definition(context)
2129
+ cid = if context.vocab && id.start_with?(context.vocab)
2130
+ # Nothing to return unless it's the same as the vocab
2131
+ id == context.vocab ? context.vocab : id.to_s[context.vocab.length..-1]
2132
+ else
2133
+ # Find a term to act as a prefix
2134
+ iri, prefix = context.iri_to_term.detect {|i,p| id.to_s.start_with?(i.to_s)}
2135
+ iri && iri != id ? "#{prefix}:#{id.to_s[iri.length..-1]}" : id
2136
+ end
2137
+
2138
+ if simple?
2139
+ cid.to_s unless cid == term && context.vocab
2140
+ else
2141
+ defn = {}
2142
+ defn[reverse_property ? '@reverse' : '@id'] = cid.to_s unless cid == term && !reverse_property
2143
+ if type_mapping
2144
+ defn['@type'] = if KEYWORDS.include?(type_mapping)
2145
+ type_mapping
2146
+ else
2147
+ context.compact_iri(type_mapping, vocab: true)
2148
+ end
2149
+ end
2150
+
2151
+ cm = Array(container_mapping)
2152
+ cm << "@set" if as_set? && !cm.include?("@set")
2153
+ cm = cm.first if cm.length == 1
2154
+ defn['@container'] = cm unless cm.empty?
2155
+ # Language set as false to be output as null
2156
+ defn['@language'] = (@language_mapping ? @language_mapping : nil) unless @language_mapping.nil?
2157
+ defn['@direction'] = (@direction_mapping ? @direction_mapping : nil) unless @direction_mapping.nil?
2158
+ defn['@context'] = @context if @context
2159
+ defn['@nest'] = @nest if @nest
2160
+ defn['@index'] = @index if @index
2161
+ defn['@prefix'] = @prefix unless @prefix.nil?
2162
+ defn
2163
+ end
2164
+ end
2165
+
2166
+ ##
2167
+ # Turn this into a source for a new instantiation
2168
+ # FIXME: context serialization
2169
+ # @return [String]
2170
+ def to_rb
2171
+ defn = [%(TermDefinition.new\(#{term.inspect})]
2172
+ %w(id index type_mapping container_mapping language_mapping direction_mapping reverse_property nest simple prefix context protected).each do |acc|
2173
+ v = instance_variable_get("@#{acc}".to_sym)
2174
+ v = v.to_s if v.is_a?(RDF::Term)
2175
+ if acc == 'container_mapping'
2176
+ v = v.to_a
2177
+ v << '@set' if as_set?
2178
+ v = v.first if v.length <= 1
2179
+ end
2180
+ defn << "#{acc}: #{v.inspect}" if v
2181
+ end
2182
+ defn.join(', ') + ")"
2183
+ end
2184
+
2185
+ # If container mapping was defined along with @set
2186
+ # @return [Boolean]
2187
+ def as_set?; @as_set || false; end
2188
+
2189
+ # Check if term definitions are identical, modulo @protected
2190
+ # @return [Boolean]
2191
+ def ==(other)
2192
+ other.is_a?(TermDefinition) &&
2193
+ id == other.id &&
2194
+ term == other.term &&
2195
+ type_mapping == other.type_mapping &&
2196
+ container_mapping == other.container_mapping &&
2197
+ nest == other.nest &&
2198
+ language_mapping == other.language_mapping &&
2199
+ direction_mapping == other.direction_mapping &&
2200
+ reverse_property == other.reverse_property &&
2201
+ simple == other.simple &&
2202
+ index == other.index &&
2203
+ context == other.context &&
2204
+ prefix? == other.prefix? &&
2205
+ as_set? == other.as_set?
2206
+ end
2207
+
2208
+ def inspect
2209
+ v = %w([TD)
2210
+ v << "id=#{@id}"
2211
+ v << "index=#{index.inspect}" unless index.nil?
2212
+ v << "term=#{@term}"
2213
+ v << "rev" if reverse_property
2214
+ v << "container=#{container_mapping}" if container_mapping
2215
+ v << "as_set=#{as_set?.inspect}"
2216
+ v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
2217
+ v << "dir=#{direction_mapping.inspect}" unless direction_mapping.nil?
2218
+ v << "type=#{type_mapping}" unless type_mapping.nil?
2219
+ v << "nest=#{nest.inspect}" unless nest.nil?
2220
+ v << "simple=true" if @simple
2221
+ v << "protected=true" if @protected
2222
+ v << "prefix=#{@prefix.inspect}" unless @prefix.nil?
2223
+ v << "has-context" unless context.nil?
2224
+ v.join(" ") + "]"
2225
+ end
2226
+ end
2168
2227
  end
2169
2228
  end