json-ld 3.1.3 → 3.1.4

Sign up to get free protection for your applications and to get access to all the features.
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