json-ld 3.0.2 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +1 -1
  3. data/README.md +90 -53
  4. data/UNLICENSE +1 -1
  5. data/VERSION +1 -1
  6. data/bin/jsonld +4 -4
  7. data/lib/json/ld.rb +27 -10
  8. data/lib/json/ld/api.rb +325 -96
  9. data/lib/json/ld/compact.rb +75 -27
  10. data/lib/json/ld/conneg.rb +188 -0
  11. data/lib/json/ld/context.rb +677 -292
  12. data/lib/json/ld/expand.rb +240 -75
  13. data/lib/json/ld/flatten.rb +5 -3
  14. data/lib/json/ld/format.rb +19 -19
  15. data/lib/json/ld/frame.rb +135 -85
  16. data/lib/json/ld/from_rdf.rb +44 -17
  17. data/lib/json/ld/html/nokogiri.rb +151 -0
  18. data/lib/json/ld/html/rexml.rb +186 -0
  19. data/lib/json/ld/reader.rb +25 -5
  20. data/lib/json/ld/resource.rb +2 -2
  21. data/lib/json/ld/streaming_writer.rb +3 -1
  22. data/lib/json/ld/to_rdf.rb +47 -17
  23. data/lib/json/ld/utils.rb +4 -2
  24. data/lib/json/ld/writer.rb +75 -14
  25. data/spec/api_spec.rb +13 -34
  26. data/spec/compact_spec.rb +968 -9
  27. data/spec/conneg_spec.rb +373 -0
  28. data/spec/context_spec.rb +447 -53
  29. data/spec/expand_spec.rb +1872 -416
  30. data/spec/flatten_spec.rb +434 -47
  31. data/spec/frame_spec.rb +979 -344
  32. data/spec/from_rdf_spec.rb +305 -5
  33. data/spec/spec_helper.rb +177 -0
  34. data/spec/streaming_writer_spec.rb +4 -4
  35. data/spec/suite_compact_spec.rb +2 -2
  36. data/spec/suite_expand_spec.rb +14 -2
  37. data/spec/suite_flatten_spec.rb +10 -2
  38. data/spec/suite_frame_spec.rb +3 -2
  39. data/spec/suite_from_rdf_spec.rb +2 -2
  40. data/spec/suite_helper.rb +55 -20
  41. data/spec/suite_html_spec.rb +22 -0
  42. data/spec/suite_http_spec.rb +35 -0
  43. data/spec/suite_remote_doc_spec.rb +2 -2
  44. data/spec/suite_to_rdf_spec.rb +14 -3
  45. data/spec/support/extensions.rb +5 -1
  46. data/spec/test-files/test-4-input.json +3 -3
  47. data/spec/test-files/test-5-input.json +2 -2
  48. data/spec/test-files/test-8-framed.json +14 -18
  49. data/spec/to_rdf_spec.rb +606 -16
  50. data/spec/writer_spec.rb +5 -5
  51. metadata +144 -88
@@ -5,7 +5,13 @@ module JSON::LD
5
5
  include Utils
6
6
 
7
7
  # The following constant is used to reduce object allocations in #compact below
8
+ CONTAINER_MAPPING_ID = %w(@id).freeze
9
+ CONTAINER_MAPPING_INDEX = %w(@index).freeze
10
+ CONTAINER_MAPPING_LANGUAGE = %w(@language).freeze
8
11
  CONTAINER_MAPPING_LANGUAGE_INDEX_ID_TYPE = Set.new(%w(@language @index @id @type)).freeze
12
+ CONTAINER_MAPPING_LIST = %w(@list).freeze
13
+ CONTAINER_MAPPING_TYPE = %w(@type).freeze
14
+ EXPANDED_PROPERTY_DIRECTION_INDEX_LANGUAGE_VALUE = %w(@direction @index @language @value).freeze
9
15
 
10
16
  ##
11
17
  # 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.
@@ -24,8 +30,6 @@ module JSON::LD
24
30
 
25
31
  # If the term definition for active property itself contains a context, use that for compacting values.
26
32
  input_context = self.context
27
- td = self.context.term_definitions[property] if property
28
- self.context = (td && td.context && self.context.parse(td.context)) || input_context
29
33
 
30
34
  case element
31
35
  when Array
@@ -47,22 +51,30 @@ module JSON::LD
47
51
  # Otherwise element is a JSON object.
48
52
 
49
53
  # @null objects are used in framing
50
- return nil if element.has_key?('@null')
54
+ return nil if element.key?('@null')
55
+
56
+ # Revert any previously type-scoped (non-preserved) context
57
+ if context.previous_context && !element.key?('@value') && element.keys != %w(@id)
58
+ self.context = context.previous_context
59
+ end
60
+
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
62
+ td = input_context.term_definitions[property] if property
63
+ self.context = context.parse(td.context, override_protected: true) if td && td.context
51
64
 
52
65
  if element.key?('@id') || element.key?('@value')
53
66
  result = context.compact_value(property, element, log_depth: @options[:log_depth])
54
- unless result.is_a?(Hash)
67
+ if !result.is_a?(Hash) || context.coerce(property) == '@json'
55
68
  #log_debug("") {"=> scalar result: #{result.inspect}"}
56
69
  return result
57
70
  end
58
71
  end
59
72
 
60
73
  # If expanded property is @list and we're contained within a list container, recursively compact this item to an array
61
- if list?(element) && context.container(property) == %w(@list)
74
+ if list?(element) && context.container(property) == CONTAINER_MAPPING_LIST
62
75
  return compact(element['@list'], property: property, ordered: ordered)
63
76
  end
64
77
 
65
-
66
78
  inside_reverse = property == '@reverse'
67
79
  result, nest_result = {}, nil
68
80
 
@@ -72,24 +84,34 @@ module JSON::LD
72
84
  map {|expanded_type| context.compact_iri(expanded_type, vocab: true)}.
73
85
  sort.
74
86
  each do |term|
75
- term_context = self.context.term_definitions[term].context if context.term_definitions[term]
76
- self.context = context.parse(term_context) if term_context
87
+ term_context = input_context.term_definitions[term].context if input_context.term_definitions[term]
88
+ self.context = context.parse(term_context, propagate: false) if term_context
77
89
  end
78
90
 
79
91
  element.keys.opt_sort(ordered: ordered).each do |expanded_property|
80
92
  expanded_value = element[expanded_property]
81
93
  #log_debug("") {"#{expanded_property}: #{expanded_value.inspect}"}
82
94
 
83
- if expanded_property == '@id' || expanded_property == '@type'
84
- compacted_value = Array(expanded_value).map do |expanded_type|
85
- context.compact_iri(expanded_type, vocab: (expanded_property == '@type'), log_depth: @options[:log_depth])
86
- end
95
+ if expanded_property == '@id'
96
+ compacted_value = Array(expanded_value).map {|expanded_id| context.compact_iri(expanded_id)}
97
+
98
+ kw_alias = context.compact_iri('@id', vocab: true)
99
+ as_array = compacted_value.length > 1
100
+ compacted_value = compacted_value.first unless as_array
101
+ result[kw_alias] = compacted_value
102
+ next
103
+ end
87
104
 
88
- compacted_value = compacted_value.first if compacted_value.length == 1
105
+ if expanded_property == '@type'
106
+ compacted_value = Array(expanded_value).map {|expanded_type| input_context.compact_iri(expanded_type, vocab: true)}
89
107
 
90
- al = context.compact_iri(expanded_property, vocab: true, quiet: true)
91
- #log_debug(expanded_property) {"result[#{al}] = #{compacted_value.inspect}"}
92
- result[al] = compacted_value
108
+ kw_alias = context.compact_iri('@type', vocab: true)
109
+ as_array = compacted_value.length > 1 ||
110
+ (context.as_array?(kw_alias) &&
111
+ !value?(element) &&
112
+ context.processingMode('json-ld-1.1'))
113
+ compacted_value = compacted_value.first unless as_array
114
+ result[kw_alias] = compacted_value
93
115
  next
94
116
  end
95
117
 
@@ -124,13 +146,13 @@ module JSON::LD
124
146
  next
125
147
  end
126
148
 
127
- if expanded_property == '@index' && context.container(property) == %w(@index)
149
+ if expanded_property == '@index' && context.container(property) == CONTAINER_MAPPING_INDEX
128
150
  #log_debug("@index") {"drop @index"}
129
151
  next
130
152
  end
131
153
 
132
- # Otherwise, if expanded property is @index, @value, or @language:
133
- if expanded_property == '@index' || expanded_property == '@value' || expanded_property == '@language'
154
+ # Otherwise, if expanded property is @direction, @index, @value, or @language:
155
+ if EXPANDED_PROPERTY_DIRECTION_INDEX_LANGUAGE_VALUE.include?(expanded_property)
134
156
  al = context.compact_iri(expanded_property, vocab: true, quiet: true)
135
157
  #log_debug(expanded_property) {"#{al} => #{expanded_value.inspect}"}
136
158
  result[al] = expanded_value
@@ -187,7 +209,7 @@ module JSON::LD
187
209
  # handle @list
188
210
  if list?(expanded_item)
189
211
  compacted_item = as_array(compacted_item)
190
- unless container == %w(@list)
212
+ unless container == CONTAINER_MAPPING_LIST
191
213
  al = context.compact_iri('@list', vocab: true, quiet: true)
192
214
  compacted_item = {al => compacted_item}
193
215
  if expanded_item.has_key?('@index')
@@ -219,7 +241,12 @@ module JSON::LD
219
241
  property_is_array: as_array)
220
242
  elsif container.include?('@graph') && simple_graph?(expanded_item)
221
243
  # container includes @graph but not @id or @index and value is a simple graph object
222
- # Drop through, where compacted_value will be added
244
+ if compacted_item.is_a?(Array) && compacted_item.length > 1
245
+ # Mutple objects in the same graph can't be represented directly, as they would be interpreted as two different graphs. Need to wrap in @included.
246
+ included_key = context.compact_iri('@included', vocab: true).to_s
247
+ compacted_item = {included_key => compacted_item}
248
+ end
249
+ # Drop through, where compacted_item will be added
223
250
  add_value(nest_result, item_active_property, compacted_item,
224
251
  property_is_array: as_array)
225
252
  else
@@ -242,24 +269,45 @@ module JSON::LD
242
269
  c = container.first
243
270
  container_key = context.compact_iri(c, vocab: true, quiet: true)
244
271
  compacted_item = case container
245
- when %w(@id)
272
+ when CONTAINER_MAPPING_ID
246
273
  map_key = compacted_item[container_key]
247
274
  compacted_item.delete(container_key)
248
275
  compacted_item
249
- when %w(@index)
250
- map_key = expanded_item['@index']
251
- compacted_item.delete(container_key) if compacted_item.is_a?(Hash)
276
+ when CONTAINER_MAPPING_INDEX
277
+ index_key = context.term_definitions[item_active_property].index || '@index'
278
+ if index_key == '@index'
279
+ map_key = expanded_item['@index']
280
+ compacted_item.delete(container_key) if compacted_item.is_a?(Hash)
281
+ else
282
+ container_key = context.compact_iri(index_key, vocab: true, quiet: true)
283
+ map_key, *others = Array(compacted_item[container_key])
284
+ if map_key.is_a?(String)
285
+ case others.length
286
+ when 0 then compacted_item.delete(container_key)
287
+ when 1 then compacted_item[container_key] = others.first
288
+ else compacted_item[container_key] = others
289
+ end
290
+ else
291
+ map_key = context.compact_iri('@none', vocab: true, quiet: true)
292
+ end
293
+ end
294
+ # Note, if compacted_item is a node reference and key is @id-valued, then this could be compacted further.
252
295
  compacted_item
253
- when %w(@language)
296
+ when CONTAINER_MAPPING_LANGUAGE
254
297
  map_key = expanded_item['@language']
255
298
  value?(expanded_item) ? expanded_item['@value'] : compacted_item
256
- when %w(@type)
299
+ when CONTAINER_MAPPING_TYPE
257
300
  map_key, *types = Array(compacted_item[container_key])
258
301
  case types.length
259
302
  when 0 then compacted_item.delete(container_key)
260
303
  when 1 then compacted_item[container_key] = types.first
261
304
  else compacted_item[container_key] = types
262
305
  end
306
+
307
+ # if compacted_item contains a single entry who's key maps to @id, then recompact the item without @type
308
+ if compacted_item.keys.length == 1 && expanded_item.keys.include?('@id')
309
+ compacted_item = compact({'@id' => expanded_item['@id']}, property: item_active_property)
310
+ end
263
311
  compacted_item
264
312
  end
265
313
  map_key ||= context.compact_iri('@none', vocab: true, quiet: true)
@@ -0,0 +1,188 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+ require 'rack'
4
+ require 'link_header'
5
+
6
+ module JSON::LD
7
+ ##
8
+ # Rack middleware for JSON-LD content negotiation.
9
+ #
10
+ # Uses HTTP Content Negotiation to serialize `Array` and `Hash` results as JSON-LD using 'profile' accept-params to invoke appropriate JSON-LD API methods.
11
+ #
12
+ # Allows black-listing and white-listing of two-part profiles where the second part denotes a URL of a _context_ or _frame_. (See {JSON::LD::Writer.accept?})
13
+ #
14
+ # Works along with `rack-linkeddata` for serializing data which is not in the form of an `RDF::Repository`.
15
+ #
16
+ #
17
+ # @example
18
+ # use JSON::LD::Rack
19
+ #
20
+ # @see https://www.w3.org/TR/json-ld11/#iana-considerations
21
+ # @see https://www.rubydoc.info/github/rack/rack/master/file/SPEC
22
+ class ContentNegotiation
23
+ VARY = {'Vary' => 'Accept'}.freeze
24
+
25
+ # @return [#call]
26
+ attr_reader :app
27
+
28
+ ##
29
+ # * Registers JSON::LD::Rack, suitable for Sinatra application
30
+ # * adds helpers
31
+ #
32
+ # @param [Sinatra::Base] app
33
+ # @return [void]
34
+ def self.registered(app)
35
+ options = {}
36
+ app.use(JSON::LD::Rack, **options)
37
+ end
38
+
39
+ def initialize(app)
40
+ @app = app
41
+ end
42
+
43
+ ##
44
+ # Handles a Rack protocol request.
45
+ # Parses Accept header to find appropriate mime-type and sets content_type accordingly.
46
+ #
47
+ # @param [Hash{String => String}] env
48
+ # @return [Array(Integer, Hash, #each)] Status, Headers and Body
49
+ # @see http://rack.rubyforge.org/doc/SPEC.html
50
+ def call(env)
51
+ response = app.call(env)
52
+ body = response[2].respond_to?(:body) ? response[2].body : response[2]
53
+ case body
54
+ when Array, Hash
55
+ response[2] = body # Put it back in the response, it might have been a proxy
56
+ serialize(env, *response)
57
+ else response
58
+ end
59
+ end
60
+
61
+ ##
62
+ # Serializes objects as JSON-LD. Defaults to expanded form, other forms
63
+ # determined by presense of `profile` in accept-parms.
64
+ #
65
+ # @param [Hash{String => String}] env
66
+ # @param [Integer] status
67
+ # @param [Hash{String => Object}] headers
68
+ # @param [RDF::Enumerable] body
69
+ # @return [Array(Integer, Hash, #each)] Status, Headers and Body
70
+ def serialize(env, status, headers, body)
71
+ # This will only return json-ld content types, possibly with parameters
72
+ content_types = parse_accept_header(env['HTTP_ACCEPT'] || 'application/ld+json')
73
+ content_types = content_types.select do |content_type|
74
+ _, *params = content_type.split(';').map(&:strip)
75
+ accept_params = params.inject({}) do |memo, pv|
76
+ p, v = pv.split('=').map(&:strip)
77
+ memo.merge(p.downcase.to_sym => v.sub(/^["']?([^"']*)["']?$/, '\1'))
78
+ end
79
+ JSON::LD::Writer.accept?(accept_params)
80
+ end
81
+ if content_types.empty?
82
+ not_acceptable("No appropriate combinaion of media-type and parameters found")
83
+ else
84
+ ct, *params = content_types.first.split(';').map(&:strip)
85
+ accept_params = params.inject({}) do |memo, pv|
86
+ p, v = pv.split('=').map(&:strip)
87
+ memo.merge(p.downcase.to_sym => v.sub(/^["']?([^"']*)["']?$/, '\1'))
88
+ end
89
+
90
+ # Determine API method from profile
91
+ profile = accept_params[:profile].to_s.split(' ')
92
+
93
+ # Get context from Link header
94
+ links = LinkHeader.parse(env['HTTP_LINK'])
95
+ context = links.find_link(['rel', JSON_LD_NS+"context"]).href rescue nil
96
+ frame = links.find_link(['rel', JSON_LD_NS+"frame"]).href rescue nil
97
+
98
+ if profile.include?(JSON_LD_NS+"framed") && frame.nil?
99
+ return not_acceptable("framed profile without a frame")
100
+ end
101
+
102
+ # accept? already determined that there are appropriate contexts
103
+ # If profile also includes a URI which is not a namespace, use it for compaction.
104
+ context ||= Writer.default_context if profile.include?(JSON_LD_NS+"compacted")
105
+
106
+ result = if profile.include?(JSON_LD_NS+"flattened")
107
+ API.flatten(body, context)
108
+ elsif profile.include?(JSON_LD_NS+"framed")
109
+ API.frame(body, frame)
110
+ elsif context
111
+ API.compact(body, context)
112
+ elsif profile.include?(JSON_LD_NS+"expanded")
113
+ API.expand(body)
114
+ else
115
+ body
116
+ end
117
+
118
+ headers = headers.merge(VARY).merge('Content-Type' => ct)
119
+ [status, headers, [result.to_json]]
120
+ end
121
+ rescue
122
+ http_error(500, $!.message)
123
+ end
124
+
125
+ protected
126
+
127
+ ##
128
+ # Parses an HTTP `Accept` header, returning an array of MIME content
129
+ # types ordered by the precedence rules defined in HTTP/1.1 §14.1.
130
+ #
131
+ # @param [String, #to_s] header
132
+ # @return [Array<String>]
133
+ # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
134
+ def parse_accept_header(header)
135
+ entries = header.to_s.split(',')
136
+ entries = entries.
137
+ map { |e| accept_entry(e) }.
138
+ sort_by(&:last).
139
+ map(&:first)
140
+ entries.map { |e| find_content_type_for_media_range(e) }.compact
141
+ end
142
+
143
+ # Returns an array of quality, number of '*' in content-type, and number of non-'q' parameters
144
+ def accept_entry(entry)
145
+ type, *options = entry.split(';').map(&:strip)
146
+ quality = 0 # we sort smallest first
147
+ options.delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' }
148
+ [options.unshift(type).join(';'), [quality, type.count('*'), 1 - options.size]]
149
+ end
150
+
151
+ ##
152
+ # Returns a content type appropriate for the given `media_range`,
153
+ # returns `nil` if `media_range` contains a wildcard subtype
154
+ # that is not mapped.
155
+ #
156
+ # @param [String, #to_s] media_range
157
+ # @return [String, nil]
158
+ def find_content_type_for_media_range(media_range)
159
+ media_range = media_range.sub('*/*', 'application/ld+json') if media_range.to_s.start_with?('*/*')
160
+ media_range = media_range.sub('application/*', 'application/ld+json') if media_range.to_s.start_with?('application/*')
161
+ media_range = media_range.sub('application/json', 'application/ld+json') if media_range.to_s.start_with?('application/json')
162
+
163
+ media_range.start_with?('application/ld+json') ? media_range : nil
164
+ end
165
+
166
+ ##
167
+ # Outputs an HTTP `406 Not Acceptable` response.
168
+ #
169
+ # @param [String, #to_s] message
170
+ # @return [Array(Integer, Hash, #each)]
171
+ def not_acceptable(message = nil)
172
+ http_error(406, message, VARY)
173
+ end
174
+
175
+ ##
176
+ # Outputs an HTTP `4xx` or `5xx` response.
177
+ #
178
+ # @param [Integer, #to_i] code
179
+ # @param [String, #to_s] message
180
+ # @param [Hash{String => String}] headers
181
+ # @return [Array(Integer, Hash, #each)]
182
+ def http_error(code, message = nil, headers = {})
183
+ message = [code, Rack::Utils::HTTP_STATUS_CODES[code]].join(' ') +
184
+ (message.nil? ? "\n" : " (#{message})\n")
185
+ [code, {'Content-Type' => "text/plain"}.merge(headers), [message]]
186
+ end
187
+ end
188
+ end
@@ -24,6 +24,14 @@ module JSON::LD
24
24
  def add_preloaded(url, context = nil, &block)
25
25
  PRELOADED[url.to_s.freeze] = context || block
26
26
  end
27
+
28
+ ##
29
+ # Alias a previousliy loaded context
30
+ # @param [String, RDF::URI] a
31
+ # @param [String, RDF::URI] url
32
+ def alias_preloaded(a, url)
33
+ PRELOADED[a.to_s.freeze] = PRELOADED[url.to_s.freeze]
34
+ end
27
35
  end
28
36
 
29
37
  # Term Definitions specify how properties and values have to be interpreted as well as the current vocabulary mapping and the default language
@@ -38,16 +46,20 @@ module JSON::LD
38
46
  attr_accessor :type_mapping
39
47
 
40
48
  # Base container mapping, without @set
41
- # @return Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'> Container mapping
49
+ # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] Container mapping
42
50
  attr_reader :container_mapping
43
51
 
44
52
  # @return [String] Term used for nest properties
45
53
  attr_accessor :nest
46
54
 
47
- # Language mapping of term, `false` is used if there is explicitly no language mapping for this term.
55
+ # Language mapping of term, `false` is used if there is an explicit language mapping for this term.
48
56
  # @return [String] Language mapping
49
57
  attr_accessor :language_mapping
50
58
 
59
+ # Direction of term, `false` is used if there is explicit direction mapping mapping for this term.
60
+ # @return ["ltr", "rtl"] direction_mapping
61
+ attr_accessor :direction_mapping
62
+
51
63
  # @return [Boolean] Reverse Property
52
64
  attr_accessor :reverse_property
53
65
 
@@ -55,6 +67,10 @@ module JSON::LD
55
67
  # @return [Boolean]
56
68
  attr_accessor :simple
57
69
 
70
+ # Property used for data indexing; defaults to @index
71
+ # @return [Boolean]
72
+ attr_accessor :index
73
+
58
74
  # Indicate that term may be used as a prefix
59
75
  attr_writer :prefix
60
76
 
@@ -62,6 +78,10 @@ module JSON::LD
62
78
  # @return [Hash{String => Object}]
63
79
  attr_accessor :context
64
80
 
81
+ # Term is protected.
82
+ # @return [Boolean]
83
+ attr_writer :protected
84
+
65
85
  # This is a simple term definition, not an expanded term definition
66
86
  # @return [Boolean] simple
67
87
  def simple?; simple; end
@@ -76,8 +96,11 @@ module JSON::LD
76
96
  # @param [String] type_mapping Type mapping
77
97
  # @param [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] container_mapping
78
98
  # @param [String] language_mapping
79
- # Language mapping of term, `false` is used if there is explicitly no language mapping for this term
99
+ # Language mapping of term, `false` is used if there is an explicit language mapping for this term
100
+ # @param ["ltr", "rtl"] direction_mapping
101
+ # Direction mapping of term, `false` is used if there is an explicit direction mapping for this term
80
102
  # @param [Boolean] reverse_property
103
+ # @param [Boolean] protected
81
104
  # @param [String] nest term used for nest properties
82
105
  # @param [Boolean] simple
83
106
  # This is a simple term definition, not an expanded term definition
@@ -85,26 +108,36 @@ module JSON::LD
85
108
  # Term may be used as a prefix
86
109
  def initialize(term,
87
110
  id: nil,
111
+ index: nil,
88
112
  type_mapping: nil,
89
113
  container_mapping: nil,
90
114
  language_mapping: nil,
115
+ direction_mapping: nil,
91
116
  reverse_property: false,
92
117
  nest: nil,
118
+ protected: false,
93
119
  simple: false,
94
120
  prefix: nil,
95
121
  context: nil)
96
122
  @term = term
97
123
  @id = id.to_s unless id.nil?
124
+ @index = index.to_s unless index.nil?
98
125
  @type_mapping = type_mapping.to_s unless type_mapping.nil?
99
126
  self.container_mapping = container_mapping
100
127
  @language_mapping = language_mapping unless language_mapping.nil?
128
+ @direction_mapping = direction_mapping unless direction_mapping.nil?
101
129
  @reverse_property = reverse_property
130
+ @protected = protected
102
131
  @nest = nest unless nest.nil?
103
132
  @simple = simple
104
133
  @prefix = prefix unless prefix.nil?
105
134
  @context = context unless context.nil?
106
135
  end
107
136
 
137
+ # Term is protected.
138
+ # @return [Boolean]
139
+ def protected?; !!@protected; end
140
+
108
141
  # Set container mapping, from an array which may include @set
109
142
  def container_mapping=(mapping)
110
143
  mapping = Array(mapping)
@@ -113,6 +146,7 @@ module JSON::LD
113
146
  mapping.delete('@set')
114
147
  end
115
148
  @container_mapping = mapping.sort
149
+ @index ||= '@index' if mapping.include?('@index')
116
150
  end
117
151
 
118
152
  ##
@@ -143,14 +177,16 @@ module JSON::LD
143
177
  end
144
178
  end
145
179
 
146
- cm = (Array(container_mapping) + (as_set? ? %w(@set) : [])).compact
180
+ cm = Array(container_mapping)
181
+ cm << "@set" if as_set? && !cm.include?("@set")
147
182
  cm = cm.first if cm.length == 1
148
183
  defn['@container'] = cm unless cm.empty?
149
184
  # Language set as false to be output as null
150
185
  defn['@language'] = (@language_mapping ? @language_mapping : nil) unless @language_mapping.nil?
151
186
  defn['@context'] = @context if @context
152
187
  defn['@nest'] = @nest if @nest
153
- defn['@prefix'] = @prefix unless @prefix.nil? || (context.processingMode || 'json-ld-1.0') == 'json-ld-1.0'
188
+ defn['@index'] = @index if @index
189
+ defn['@prefix'] = @prefix unless @prefix.nil?
154
190
  defn
155
191
  end
156
192
  end
@@ -161,7 +197,7 @@ module JSON::LD
161
197
  # @return [String]
162
198
  def to_rb
163
199
  defn = [%(TermDefinition.new\(#{term.inspect})]
164
- %w(id type_mapping container_mapping language_mapping reverse_property nest simple prefix context).each do |acc|
200
+ %w(id index type_mapping container_mapping language_mapping direction_mapping reverse_property nest simple prefix context protected).each do |acc|
165
201
  v = instance_variable_get("@#{acc}".to_sym)
166
202
  v = v.to_s if v.is_a?(RDF::Term)
167
203
  if acc == 'container_mapping'
@@ -177,17 +213,39 @@ module JSON::LD
177
213
  # @return [Boolean]
178
214
  def as_set?; @as_set || false; end
179
215
 
216
+ # Check if term definitions are identical, modulo @protected
217
+ # @return [Boolean]
218
+ def ==(other)
219
+ other.is_a?(TermDefinition) &&
220
+ id == other.id &&
221
+ term == other.term &&
222
+ type_mapping == other.type_mapping &&
223
+ container_mapping == other.container_mapping &&
224
+ nest == other.nest &&
225
+ language_mapping == other.language_mapping &&
226
+ direction_mapping == other.direction_mapping &&
227
+ reverse_property == other.reverse_property &&
228
+ simple == other.simple &&
229
+ index == other.index &&
230
+ context == other.context &&
231
+ prefix? == other.prefix? &&
232
+ as_set? == other.as_set?
233
+ end
234
+
180
235
  def inspect
181
236
  v = %w([TD)
182
237
  v << "id=#{@id}"
238
+ v << "index=#{index.inspect}" unless index.nil?
183
239
  v << "term=#{@term}"
184
240
  v << "rev" if reverse_property
185
241
  v << "container=#{container_mapping}" if container_mapping
186
242
  v << "as_set=#{as_set?.inspect}"
187
243
  v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
244
+ v << "dir=#{direction_mapping.inspect}" unless direction_mapping.nil?
188
245
  v << "type=#{type_mapping}" unless type_mapping.nil?
189
246
  v << "nest=#{nest.inspect}" unless nest.nil?
190
247
  v << "simple=true" if @simple
248
+ v << "protected=true" if @protected
191
249
  v << "prefix=#{@prefix.inspect}" unless @prefix.nil?
192
250
  v << "has-context" unless context.nil?
193
251
  v.join(" ") + "]"
@@ -214,13 +272,26 @@ module JSON::LD
214
272
  # @return [Hash{RDF::URI => String}] Reverse mappings from IRI to term only for terms, not CURIEs XXX
215
273
  attr_accessor :iri_to_term
216
274
 
275
+ # Previous definition for this context. This is used for rolling back type-scoped contexts.
276
+ # @return [Context]
277
+ attr_accessor :previous_context
278
+
279
+ # Context is property-scoped
280
+ # @return [Boolean]
281
+ attr_accessor :property_scoped
282
+
217
283
  # Default language
218
284
  #
219
- #
220
285
  # This adds a language to plain strings that aren't otherwise coerced
221
286
  # @return [String]
222
287
  attr_reader :default_language
223
-
288
+
289
+ # Default direction
290
+ #
291
+ # This adds a direction to plain strings that aren't otherwise coerced
292
+ # @return ["lrt", "rtl"]
293
+ attr_reader :default_direction
294
+
224
295
  # Default vocabulary
225
296
  #
226
297
  # Sets the default vocabulary used for expanding terms which
@@ -237,9 +308,6 @@ module JSON::LD
237
308
  # @return [BlankNodeNamer]
238
309
  attr_accessor :namer
239
310
 
240
- # @return [String]
241
- attr_accessor :processingMode
242
-
243
311
  ##
244
312
  # Create a new context by parsing a context.
245
313
  #
@@ -249,8 +317,8 @@ module JSON::LD
249
317
  # @raise [JsonLdError]
250
318
  # on a remote context load error, syntax error, or a reference to a term which is not defined.
251
319
  # @return [Context]
252
- def self.parse(local_context, **options)
253
- self.new(options).parse(local_context)
320
+ def self.parse(local_context, protected: false, override_protected: false, propagate: true, **options)
321
+ self.new(**options).parse(local_context, protected: false, override_protected: override_protected, propagate: propagate)
254
322
  end
255
323
 
256
324
  ##
@@ -274,8 +342,7 @@ module JSON::LD
274
342
  @base = @doc_base = RDF::URI(options[:base]).dup
275
343
  @doc_base.canonicalize! if options[:canonicalize]
276
344
  end
277
- options[:documentLoader] ||= JSON::LD::API.method(:documentLoader)
278
- @processingMode ||= options[:processingMode]
345
+ self.processingMode = options[:processingMode] if options.has_key?(:processingMode)
279
346
  @term_definitions = {}
280
347
  @iri_to_term = {
281
348
  RDF.to_uri.to_s => "rdf",
@@ -293,11 +360,11 @@ module JSON::LD
293
360
  end
294
361
 
295
362
  self.vocab = options[:vocab] if options[:vocab]
296
- self.default_language = options[:language] if options[:language]
363
+ self.default_language = options[:language] if options[:language] =~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
297
364
  @term_definitions = options[:term_definitions] if options[:term_definitions]
298
365
 
299
366
  #log_debug("init") {"iri_to_term: #{iri_to_term.inspect}"}
300
-
367
+
301
368
  yield(self) if block_given?
302
369
  end
303
370
 
@@ -310,7 +377,7 @@ module JSON::LD
310
377
  end
311
378
 
312
379
  # @param [String] value must be an absolute IRI
313
- def base=(value)
380
+ def base=(value, **options)
314
381
  if value
315
382
  raise JsonLdError::InvalidBaseIRI, "@base must be a string: #{value.inspect}" unless value.is_a?(String) || value.is_a?(RDF::URI)
316
383
  value = RDF::URI(value).dup
@@ -326,38 +393,91 @@ module JSON::LD
326
393
  end
327
394
 
328
395
  # @param [String] value
329
- def default_language=(value)
330
- @default_language = if value
331
- raise JsonLdError::InvalidDefaultLanguage, "@language must be a string: #{value.inspect}" unless value.is_a?(String)
332
- value.downcase
396
+ def default_language=(value, **options)
397
+ @default_language = case value
398
+ when String
399
+ # Warn on an invalid language tag, unless :validate is true, in which case it's an error
400
+ if value !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
401
+ warn "@language must be valid BCP47: #{value.inspect}"
402
+ end
403
+ options[:lowercaseLanguage] ? value.downcase : value
404
+ when nil
405
+ nil
406
+ else
407
+ raise JsonLdError::InvalidDefaultLanguage, "@language must be a string: #{value.inspect}"
408
+ end
409
+ end
410
+
411
+ # @param [String] value
412
+ def default_direction=(value, **options)
413
+ @default_direction = if value
414
+ raise JsonLdError::InvalidBaseDirection, "@direction must be one or 'ltr', or 'rtl': #{value.inspect}" unless %w(ltr rtl).include?(value)
415
+ value
333
416
  else
334
417
  nil
335
418
  end
336
419
  end
337
420
 
421
+ ##
422
+ # Retrieve, or check processing mode.
423
+ #
424
+ # * With no arguments, retrieves the current set processingMode.
425
+ # * With an argument, verifies that the processingMode is at least that provided, either as an integer, or a string of the form "json-ld-1.x"
426
+ # * If expecting 1.1, and not set, it has the side-effect of setting mode to json-ld-1.1.
427
+ #
428
+ # @param [String, Number] expected (nil)
429
+ # @return [String]
430
+ def processingMode(expected = nil)
431
+ case expected
432
+ when 1.0, 'json-ld-1.0'
433
+ @processingMode == 'json-ld-1.0'
434
+ when 1.1, 'json-ld-1.1'
435
+ @processingMode ||= 'json-ld-1.1'
436
+ @processingMode == 'json-ld-1.1'
437
+ when nil
438
+ @processingMode
439
+ else
440
+ false
441
+ end
442
+ end
443
+
444
+ ##
445
+ # Set processing mode.
446
+ #
447
+ # * With an argument, verifies that the processingMode is at least that provided, either as an integer, or a string of the form "json-ld-1.x"
448
+ #
338
449
  # If contex has a @version member, it's value MUST be 1.1, otherwise an "invalid @version value" has been detected, and processing is aborted.
339
450
  # If processingMode has been set, and it is not "json-ld-1.1", a "processing mode conflict" has been detecting, and processing is aborted.
340
- # @param [Number] vaule must be a decimal number
341
- def version=(value)
451
+ #
452
+ # @param [String, Number] expected
453
+ # @return [String]
454
+ # @raise [JsonLdError::ProcessingModeConflict]
455
+ def processingMode=(value = nil, **options)
456
+ value = "json-ld-1.1" if value == 1.1
342
457
  case value
343
- when 1.1
344
- if processingMode && processingMode < "json-ld-1.1"
345
- raise JsonLdError::ProcessingModeConflict, "#{value} not compatible with #{processingMode}"
458
+ when "json-ld-1.0", "json-ld-1.1"
459
+ if @processingMode && @processingMode != value
460
+ raise JsonLdError::ProcessingModeConflict, "#{value} not compatible with #{@processingMode}"
346
461
  end
347
- @processingMode = "json-ld-1.1"
462
+ @processingMode = value
348
463
  else
349
- raise JsonLdError::InvalidVersionValue, value
464
+ raise JsonLdError::InvalidVersionValue, value.inspect
350
465
  end
351
466
  end
352
467
 
353
468
  # If context has a @vocab member: if its value is not a valid absolute IRI or null trigger an INVALID_VOCAB_MAPPING error; otherwise set the active context's vocabulary mapping to its value and remove the @vocab member from context.
354
469
  # @param [String] value must be an absolute IRI
355
- def vocab=(value)
470
+ def vocab=(value, **options)
356
471
  @vocab = case value
357
472
  when /_:/
473
+ # BNode vocab is deprecated
474
+ warn "[DEPRECATION] Blank Node vocabularies deprecated in JSON-LD 1.1." if @options[:validate] && processingMode("json-ld-1.1")
358
475
  value
359
476
  when String, RDF::URI
360
- v = as_resource(value.to_s, base)
477
+ if (RDF::URI(value.to_s).relative? && processingMode("json-ld-1.0"))
478
+ raise JsonLdError::InvalidVocabMapping, "@vocab must be an absolute IRI in 1.0 mode: #{value.inspect}"
479
+ end
480
+ v = expand_iri(value.to_s, vocab: true, documentRelative: true)
361
481
  raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}" if !v.valid? && @options[:validate]
362
482
  v
363
483
  when nil
@@ -367,31 +487,56 @@ module JSON::LD
367
487
  end
368
488
  end
369
489
 
490
+ # Set propagation
491
+ # @note: by the time this is called, the work has already been done.
492
+ #
493
+ # @param [Boolean] value
494
+ def propagate=(value, **options)
495
+ raise JsonLdError::InvalidContextMember, "@propagate may only be set in 1.1 mode" if processingMode("json-ld-1.0")
496
+ raise JsonLdError::InvalidPropagateValue, "@propagate must be boolean valued: #{value.inspect}" unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
497
+ value
498
+ end
499
+
370
500
  # Create an Evaluation Context
371
501
  #
372
502
  # 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.
373
- #
503
+ #
374
504
  # The active context contains the active term definitions which specify how properties and values have to be interpreted as well as the current base IRI, the vocabulary mapping and the default language. Each term definition consists of an IRI mapping, a boolean flag reverse property, an optional type mapping or language mapping, and an optional container mapping. A term definition can not only be used to map a term to an IRI, but also to map a term to a keyword, in which case it is referred to as a keyword alias.
375
- #
505
+ #
376
506
  # When processing, the active context is initialized without any term definitions, vocabulary mapping, or default language. If a local context is encountered during processing, a new active context is created by cloning the existing active context. Then the information from the local context is merged into the new active context. Given that local contexts may contain references to remote contexts, this includes their retrieval.
377
- #
507
+ #
378
508
  #
379
509
  # @param [String, #read, Array, Hash, Context] local_context
510
+ # @param [Array<String>] remote_contexts
511
+ # @param [Boolean] protected Make defined terms protected (as if `@protected` were used).
512
+ # @param [Boolean] override_protected Protected terms may be cleared.
513
+ # @param [Boolean] propagate
514
+ # If false, retains any previously defined term, which can be rolled back when the descending into a new node object changes.
380
515
  # @raise [JsonLdError]
381
516
  # on a remote context load error, syntax error, or a reference to a term which is not defined.
382
517
  # @return [Context]
383
- # @see http://json-ld.org/spec/latest/json-ld-api/index.html#context-processing-algorithm
384
- def parse(local_context, remote_contexts = [])
518
+ # @see https://www.w3.org/TR/json-ld11-api/index.html#context-processing-algorithm
519
+ def parse(local_context, remote_contexts: [], protected: false, override_protected: false, propagate: true)
385
520
  result = self.dup
386
521
  result.provided_context = local_context if self.empty?
522
+ # Early check for @propagate, which can only appear in a local context
523
+ propagate = local_context.is_a?(Hash) ? local_context.fetch('@propagate', propagate) : propagate
524
+ result.previous_context ||= result.dup unless propagate
387
525
 
388
526
  local_context = as_array(local_context)
389
527
 
390
528
  local_context.each do |context|
391
529
  case context
392
530
  when nil
393
- # 3.1 If niil, set to a new empty context
394
- result = Context.new(options)
531
+ # 3.1 If the `override_protected` is false, and the active context contains protected terms, an error is raised.
532
+ if override_protected || result.term_definitions.values.none?(&:protected?)
533
+ null_context = Context.new(**options)
534
+ null_context.previous_context = result unless propagate
535
+ result = null_context
536
+ else
537
+ raise JSON::LD::JsonLdError::InvalidContextNullification,
538
+ "Attempt to clear a context with protected terms"
539
+ end
395
540
  when Context
396
541
  #log_debug("parse") {"context: #{context.inspect}"}
397
542
  result = context.dup
@@ -413,11 +558,10 @@ module JSON::LD
413
558
  #log_debug("parse") {"remote: #{context}, base: #{result.context_base || result.base}"}
414
559
 
415
560
  # 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].
416
- context = RDF::URI(result.context_base || result.base).join(context)
417
- context_canon = RDF::URI(context).canonicalize
418
- context_canon.dup.scheme = 'http'.dup if context_canon.scheme == 'https'
561
+ context = RDF::URI(result.context_base || options[:base]).join(context)
562
+ context_canon = context.canonicalize
563
+ context_canon.scheme == 'http' if context_canon.scheme == 'https'
419
564
 
420
- raise JsonLdError::RecursiveContextInclusion, "#{context}" if remote_contexts.include?(context.to_s)
421
565
  remote_contexts << context.to_s
422
566
  raise JsonLdError::ContextOverflow, "#{context}" if remote_contexts.length >= MAX_CONTEXTS_LOADED
423
567
 
@@ -436,19 +580,17 @@ module JSON::LD
436
580
  end
437
581
  context = context_no_base.merge!(PRELOADED[context_canon.to_s])
438
582
  else
439
-
440
583
  # Load context document, if it is a string
441
584
  begin
442
- context_opts = @options.dup
443
- context_opts.delete(:headers)
444
- @options[:documentLoader].call(context.to_s, context_opts) do |remote_doc|
585
+ context_opts = @options.merge(
586
+ profile: 'http://www.w3.org/ns/json-ld#context',
587
+ requestProfile: 'http://www.w3.org/ns/json-ld#context',
588
+ base: nil)
589
+ #context_opts.delete(:headers)
590
+ JSON::LD::API.loadRemoteDocument(context.to_s, **context_opts) do |remote_doc|
445
591
  # 3.2.5) Dereference context. If the dereferenced document has no top-level JSON object with an @context member, an invalid remote context has been detected and processing is aborted; otherwise, set context to the value of that member.
446
- jo = case remote_doc.document
447
- when String then MultiJson.load(remote_doc.document)
448
- else remote_doc.document
449
- end
450
- raise JsonLdError::InvalidRemoteContext, "#{context}" unless jo.is_a?(Hash) && jo.has_key?('@context')
451
- context = jo['@context']
592
+ raise JsonLdError::InvalidRemoteContext, "#{context}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
593
+ context = remote_doc.document['@context']
452
594
  end
453
595
  rescue JsonLdError::LoadingDocumentFailed => e
454
596
  #log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"}
@@ -461,8 +603,8 @@ module JSON::LD
461
603
  end
462
604
 
463
605
  # 3.2.6) Set context to the result of recursively calling this algorithm, passing context no base for active context, context for local context, and remote contexts.
464
- context = context_no_base.parse(context, remote_contexts.dup)
465
- PRELOADED[context_canon.to_s] = context
606
+ context = context_no_base.parse(context, remote_contexts: remote_contexts.dup, protected: protected, override_protected: override_protected, propagate: propagate)
607
+ PRELOADED[context_canon.to_s] = context.dup
466
608
  context.provided_context = result.provided_context
467
609
  end
468
610
  context.base ||= result.base
@@ -471,25 +613,58 @@ module JSON::LD
471
613
  when Hash
472
614
  context = context.dup # keep from modifying a hash passed as a param
473
615
 
616
+ # This counts on hash elements being processed in order
474
617
  {
475
- '@base' => :base=,
476
- '@language' => :default_language=,
477
- '@version' => :version=,
478
- '@vocab' => :vocab=,
618
+ '@version' => :processingMode=,
619
+ '@import' => nil,
620
+ '@base' => :base=,
621
+ '@direction' => :default_direction=,
622
+ '@language' => :default_language=,
623
+ '@propagate' => :propagate=,
624
+ '@vocab' => :vocab=,
479
625
  }.each do |key, setter|
480
- v = context.fetch(key, false)
481
- unless v == false
482
- context.delete(key)
483
- #log_debug("parse") {"Set #{key} to #{v.inspect}"}
484
- result.send(setter, v)
626
+ next unless context.has_key?(key)
627
+ if key == '@import'
628
+ # Retrieve remote context and merge the remaining context object into the result.
629
+ raise JsonLdError::InvalidContextMember, "@import may only be used in 1.1 mode}" if result.processingMode("json-ld-1.0")
630
+ raise JsonLdError::InvalidImportValue, "@import must be a string: #{context['@import'].inspect}" unless context['@import'].is_a?(String)
631
+ source = RDF::URI(result.context_base || result.base).join(context['@import'])
632
+ begin
633
+ context_opts = @options.merge(
634
+ profile: 'http://www.w3.org/ns/json-ld#context',
635
+ requestProfile: 'http://www.w3.org/ns/json-ld#context',
636
+ base: nil)
637
+ context_opts.delete(:headers)
638
+ JSON::LD::API.loadRemoteDocument(source, **context_opts) do |remote_doc|
639
+ # Dereference source. If the dereferenced document has no top-level JSON object with an @context member, an invalid remote context has been detected and processing is aborted; otherwise, set context to the value of that member.
640
+ raise JsonLdError::InvalidRemoteContext, "#{source}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
641
+ import_context = remote_doc.document['@context']
642
+ raise JsonLdError::InvalidRemoteContext, "#{import_context.to_json} must be an object" unless import_context.is_a?(Hash)
643
+ raise JsonLdError::InvalidContextMember, "#{import_context.to_json} must not include @import entry" if import_context.has_key?('@import')
644
+ context.delete(key)
645
+ context = import_context.merge(context)
646
+ end
647
+ rescue JsonLdError::LoadingDocumentFailed => e
648
+ raise JsonLdError::LoadingRemoteContextFailed, "#{source}: #{e.message}", e.backtrace
649
+ rescue JsonLdError
650
+ raise
651
+ rescue StandardError => e
652
+ raise JsonLdError::LoadingRemoteContextFailed, "#{source}: #{e.message}", e.backtrace
653
+ end
654
+ else
655
+ result.send(setter, context[key], remote_contexts: remote_contexts, protected: context.fetch('@protected', protected))
485
656
  end
657
+ context.delete(key)
486
658
  end
487
659
 
488
660
  defined = {}
489
661
 
490
662
  # For each key-value pair in context invoke the Create Term Definition subalgorithm, passing result for active context, context for local context, key, and defined
491
663
  context.each_key do |key|
492
- result.create_term_definition(context, key, defined)
664
+ # ... where key is not @base, @vocab, @language, or @version
665
+ result.create_term_definition(context, key, defined,
666
+ override_protected: override_protected,
667
+ protected: context.fetch('@protected', protected)) unless NON_TERMDEF_KEYS.include?(key)
493
668
  end
494
669
  else
495
670
  # 3.3) If context is not a JSON object, an invalid local context error has been detected and processing is aborted.
@@ -529,23 +704,32 @@ module JSON::LD
529
704
 
530
705
  # The following constants are used to reduce object allocations in #create_term_definition below
531
706
  ID_NULL_OBJECT = { '@id' => nil }.freeze
707
+ NON_TERMDEF_KEYS = Set.new(%w(@base @direction @language @protected @version @vocab)).freeze
532
708
  JSON_LD_10_EXPECTED_KEYS = Set.new(%w(@container @id @language @reverse @type)).freeze
533
- JSON_LD_EXPECTED_KEYS = Set.new(%w(@container @context @id @language @nest @prefix @reverse @type)).freeze
709
+ JSON_LD_11_EXPECTED_KEYS = Set.new(%w(@context @direction @index @nest @prefix @protected)).freeze
710
+ JSON_LD_EXPECTED_KEYS = (JSON_LD_10_EXPECTED_KEYS + JSON_LD_11_EXPECTED_KEYS).freeze
711
+ JSON_LD_10_TYPE_VALUES = Set.new(%w(@id @vocab)).freeze
712
+ JSON_LD_11_TYPE_VALUES = Set.new(%w(@json @none)).freeze
713
+ PREFIX_URI_ENDINGS = Set.new(%w(: / ? # [ ] @)).freeze
534
714
 
535
715
  ##
536
716
  # Create Term Definition
537
717
  #
538
718
  # Term definitions are created by parsing the information in the given local context for the given term. If the given term is a compact IRI, it may omit an IRI mapping by depending on its prefix having its own term definition. If the prefix is a key in the local context, then its term definition must first be created, through recursion, before continuing. Because a term definition can depend on other term definitions, a mechanism must be used to detect cyclical dependencies. The solution employed here uses a map, defined, that keeps track of whether or not a term has been defined or is currently in the process of being defined. This map is checked before any recursion is attempted.
539
- #
719
+ #
540
720
  # After all dependencies for a term have been defined, the rest of the information in the local context for the given term is taken into account, creating the appropriate IRI mapping, container mapping, and type mapping or language mapping for the term.
541
721
  #
542
722
  # @param [Hash] local_context
543
723
  # @param [String] term
544
724
  # @param [Hash] defined
725
+ # @param [Boolean] protected if true, causes all terms to be marked protected
726
+ # @param [Boolean] override_protected Protected terms may be cleared.
727
+ # @param [Boolean] propagate
728
+ # Context is propagated across node objects.
545
729
  # @raise [JsonLdError]
546
730
  # Represents a cyclical term dependency
547
- # @see http://json-ld.org/spec/latest/json-ld-api/index.html#create-term-definition
548
- def create_term_definition(local_context, term, defined)
731
+ # @see https://www.w3.org/TR/json-ld11-api/index.html#create-term-definition
732
+ def create_term_definition(local_context, term, defined, override_protected: false, protected: false)
549
733
  # Expand a string value, unless it matches a keyword
550
734
  #log_debug("create_term_definition") {"term = #{term.inspect}"}
551
735
 
@@ -558,174 +742,267 @@ module JSON::LD
558
742
  raise JsonLdError::CyclicIRIMapping, "Cyclical term dependency found: #{term.inspect}"
559
743
  end
560
744
 
745
+ # Initialize value to a the value associated with the key term in local context.
746
+ value = local_context.fetch(term, false)
747
+ simple_term = value.is_a?(String) || value.nil?
748
+
561
749
  # Since keywords cannot be overridden, term must not be a keyword. Otherwise, an invalid value has been detected, which is an error.
562
- if KEYWORDS.include?(term) && (term != '@vocab' && term != '@language' && term != '@version')
750
+ if term == '@type' &&
751
+ value.is_a?(Hash) &&
752
+ processingMode("json-ld-1.1") &&
753
+ (value.keys - %w(@container @protected)).empty? &&
754
+ value.fetch('@container', '@set') == '@set'
755
+ # thes are the only cases were redefining a keyword is allowed
756
+ elsif KEYWORDS.include?(term) # TODO anything that looks like a keyword
563
757
  raise JsonLdError::KeywordRedefinition, "term must not be a keyword: #{term.inspect}" if
564
758
  @options[:validate]
759
+ elsif term.to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
760
+ warn "Terms beginning with '@' are reserved for future use and ignored: #{term}."
761
+ return
565
762
  elsif !term_valid?(term) && @options[:validate]
566
763
  raise JsonLdError::InvalidTermDefinition, "term is invalid: #{term.inspect}"
567
764
  end
568
765
 
766
+ value = {'@id' => value} if simple_term
767
+
569
768
  # Remove any existing term definition for term in active context.
570
- term_definitions.delete(term)
769
+ previous_definition = term_definitions[term]
770
+ if previous_definition && previous_definition.protected? && !override_protected
771
+ # Check later to detect identical redefinition
772
+ else
773
+ term_definitions.delete(term) if previous_definition
774
+ end
571
775
 
572
- # Initialize value to a the value associated with the key term in local context.
573
- value = local_context.fetch(term, false)
574
- simple_term = value.is_a?(String)
575
- value = {'@id' => value} if simple_term
776
+ raise JsonLdError::InvalidTermDefinition, "Term definition for #{term.inspect} is an #{value.class} on term #{term.inspect}" unless value.is_a?(Hash)
576
777
 
577
- case value
578
- when nil, ID_NULL_OBJECT
579
- # If value equals null or value is a JSON object containing the key-value pair (@id-null), then set the term definition in active context to null, set the value associated with defined's key term to true, and return.
580
- #log_debug("") {"=> nil"}
581
- term_definitions[term] = TermDefinition.new(term)
582
- defined[term] = true
583
- return
584
- when Hash
585
- #log_debug("") {"Hash[#{term.inspect}] = #{value.inspect}"}
586
- definition = TermDefinition.new(term)
587
- definition.simple = simple_term
588
-
589
- if options[:validate]
590
- expected_keys = case processingMode
591
- when "json-ld-1.0", nil then JSON_LD_10_EXPECTED_KEYS
592
- else JSON_LD_EXPECTED_KEYS
593
- end
778
+ #log_debug("") {"Hash[#{term.inspect}] = #{value.inspect}"}
779
+ definition = TermDefinition.new(term)
780
+ definition.simple = simple_term
781
+
782
+ expected_keys = case processingMode
783
+ when "json-ld-1.0" then JSON_LD_10_EXPECTED_KEYS
784
+ else JSON_LD_EXPECTED_KEYS
785
+ end
594
786
 
595
- if value.any? { |key, _| !expected_keys.include?(key) }
596
- extra_keys = value.keys - expected_keys.to_a
597
- raise JsonLdError::InvalidTermDefinition, "Term definition for #{term.inspect} has unexpected keys: #{extra_keys.join(', ')}"
787
+ # Any of these keys cause us to process as json-ld-1.1, unless otherwise set
788
+ if processingMode.nil? && value.any? { |key, _| !JSON_LD_11_EXPECTED_KEYS.include?(key) }
789
+ processingMode('json-ld-11')
790
+ end
791
+
792
+ if value.any? { |key, _| !expected_keys.include?(key) }
793
+ extra_keys = value.keys - expected_keys.to_a
794
+ raise JsonLdError::InvalidTermDefinition, "Term definition for #{term.inspect} has unexpected keys: #{extra_keys.join(', ')}"
795
+ end
796
+
797
+ # Potentially note that the term is protected
798
+ definition.protected = value.fetch('@protected', protected)
799
+
800
+ if value.has_key?('@type')
801
+ type = value['@type']
802
+ # SPEC FIXME: @type may be nil
803
+ type = case type
804
+ when nil
805
+ type
806
+ when String
807
+ begin
808
+ expand_iri(type, vocab: true, documentRelative: false, local_context: local_context, defined: defined)
809
+ rescue JsonLdError::InvalidIRIMapping
810
+ raise JsonLdError::InvalidTypeMapping, "invalid mapping for '@type': #{type.inspect} on term #{term.inspect}"
598
811
  end
812
+ else
813
+ :error
814
+ end
815
+ if JSON_LD_11_TYPE_VALUES.include?(type) && processingMode('json-ld-1.1')
816
+ # This is okay and used in compaction in 1.1
817
+ elsif !JSON_LD_10_TYPE_VALUES.include?(type) && !(type.is_a?(RDF::URI) && type.absolute?)
818
+ raise JsonLdError::InvalidTypeMapping, "unknown mapping for '@type': #{type.inspect} on term #{term.inspect}"
599
819
  end
820
+ #log_debug("") {"type_mapping: #{type.inspect}"}
821
+ definition.type_mapping = type
822
+ end
600
823
 
601
- if value.has_key?('@type')
602
- type = value['@type']
603
- # SPEC FIXME: @type may be nil
604
- type = case type
605
- when nil
606
- type
607
- when String
608
- begin
609
- expand_iri(type, vocab: true, documentRelative: false, local_context: local_context, defined: defined)
610
- rescue JsonLdError::InvalidIRIMapping
611
- raise JsonLdError::InvalidTypeMapping, "invalid mapping for '@type': #{type.inspect} on term #{term.inspect}"
612
- end
613
- else
614
- :error
615
- end
616
- unless (type == '@id' || type == '@vocab') || type.is_a?(RDF::URI) && type.absolute?
617
- raise JsonLdError::InvalidTypeMapping, "unknown mapping for '@type': #{type.inspect} on term #{term.inspect}"
618
- end
619
- #log_debug("") {"type_mapping: #{type.inspect}"}
620
- definition.type_mapping = type
824
+ if value.has_key?('@reverse')
825
+ raise JsonLdError::InvalidReverseProperty, "unexpected key in #{value.inspect} on term #{term.inspect}" if
826
+ value.key?('@id') || value.key?('@nest')
827
+ raise JsonLdError::InvalidIRIMapping, "expected value of @reverse to be a string: #{value['@reverse'].inspect} on term #{term.inspect}" unless
828
+ value['@reverse'].is_a?(String)
829
+
830
+ # Otherwise, set the IRI mapping of definition to the result of using the IRI Expansion algorithm, passing active context, the value associated with the @reverse key for value, true for vocab, true for document relative, local context, and defined. If the result is not an absolute IRI, i.e., it contains no colon (:), an invalid IRI mapping error has been detected and processing is aborted.
831
+ definition.id = expand_iri(value['@reverse'],
832
+ vocab: true,
833
+ local_context: local_context,
834
+ defined: defined)
835
+ raise JsonLdError::InvalidIRIMapping, "non-absolute @reverse IRI: #{definition.id} on term #{term.inspect}" unless
836
+ definition.id.is_a?(RDF::Node) || definition.id.is_a?(RDF::URI) && definition.id.absolute?
837
+
838
+ if value['@reverse'].to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
839
+ warn "Values beginning with '@' are reserved for future use and ignored: #{value['@reverse']}."
840
+ return
621
841
  end
622
842
 
623
- if value.has_key?('@reverse')
624
- raise JsonLdError::InvalidReverseProperty, "unexpected key in #{value.inspect} on term #{term.inspect}" if
625
- value.key?('@id') || value.key?('@nest')
626
- raise JsonLdError::InvalidIRIMapping, "expected value of @reverse to be a string: #{value['@reverse'].inspect} on term #{term.inspect}" unless
627
- value['@reverse'].is_a?(String)
628
-
629
- # Otherwise, set the IRI mapping of definition to the result of using the IRI Expansion algorithm, passing active context, the value associated with the @reverse key for value, true for vocab, true for document relative, local context, and defined. If the result is not an absolute IRI, i.e., it contains no colon (:), an invalid IRI mapping error has been detected and processing is aborted.
630
- definition.id = expand_iri(value['@reverse'],
631
- vocab: true,
632
- documentRelative: true,
633
- local_context: local_context,
634
- defined: defined)
635
- raise JsonLdError::InvalidIRIMapping, "non-absolute @reverse IRI: #{definition.id} on term #{term.inspect}" unless
636
- definition.id.is_a?(RDF::URI) && definition.id.absolute?
637
-
638
- # If value contains an @container member, set the container mapping of definition to its value; if its value is neither @set, @index, @type, @id, an absolute IRI nor null, an invalid reverse property error has been detected (reverse properties only support set- and index-containers) and processing is aborted.
639
- if value.has_key?('@container')
640
- container = value['@container']
641
- raise JsonLdError::InvalidReverseProperty,
642
- "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" unless
643
- container.is_a?(String) && (container == '@set' || container == '@index')
644
- definition.container_mapping = check_container(container, local_context, defined, term)
645
- end
646
- definition.reverse_property = true
647
- elsif value.has_key?('@id') && value['@id'] != term
648
- raise JsonLdError::InvalidIRIMapping, "expected value of @id to be a string: #{value['@id'].inspect} on term #{term.inspect}" unless
649
- value['@id'].is_a?(String)
650
- definition.id = expand_iri(value['@id'],
651
- vocab: true,
652
- documentRelative: true,
653
- local_context: local_context,
654
- defined: defined)
655
- raise JsonLdError::InvalidKeywordAlias, "expected value of @id to not be @context on term #{term.inspect}" if
656
- definition.id == '@context'
657
-
658
- # If id ends with a gen-delim, it may be used as a prefix for simple terms
659
- definition.prefix = true if !term.include?(':') &&
660
- definition.id.to_s.end_with?(':', '/', '?', '#', '[', ']', '@') &&
661
- simple_term
662
- elsif term.include?(':')
663
- # If term is a compact IRI with a prefix that is a key in local context then a dependency has been found. Use this algorithm recursively passing active context, local context, the prefix as term, and defined.
664
- prefix, suffix = term.split(':', 2)
665
- create_term_definition(local_context, prefix, defined) if local_context.has_key?(prefix)
666
-
667
- definition.id = if td = term_definitions[prefix]
668
- # If term's prefix has a term definition in active context, set the IRI mapping for definition to the result of concatenating the value associated with the prefix's IRI mapping and the term's suffix.
669
- td.id + suffix
670
- else
671
- # Otherwise, term is an absolute IRI. Set the IRI mapping for definition to term
672
- term
673
- end
674
- #log_debug("") {"=> #{definition.id}"}
675
- else
676
- # Otherwise, active context must have a vocabulary mapping, otherwise an invalid value has been detected, which is an error. Set the IRI mapping for definition to the result of concatenating the value associated with the vocabulary mapping and term.
677
- raise JsonLdError::InvalidIRIMapping, "relative term definition without vocab: #{term} on term #{term.inspect}" unless vocab
678
- definition.id = vocab + term
679
- #log_debug("") {"=> #{definition.id}"}
843
+ if term[1..-1].to_s.include?(':') && (term_iri = expand_iri(term)) != definition.id
844
+ raise JsonLdError::InvalidIRIMapping, "term #{term} expands to #{definition.id}, not #{term_iri}"
680
845
  end
681
846
 
682
- @iri_to_term[definition.id] = term if simple_term && definition.id
847
+ warn "[DEPRECATION] Blank Node terms deprecated in JSON-LD 1.1." if @options[:validate] && processingMode('json-ld-1.1') && definition.id.to_s.start_with?("_:")
683
848
 
849
+ # If value contains an @container member, set the container mapping of definition to its value; if its value is neither @set, @index, @type, @id, an absolute IRI nor null, an invalid reverse property error has been detected (reverse properties only support set- and index-containers) and processing is aborted.
684
850
  if value.has_key?('@container')
685
- #log_debug("") {"container_mapping: #{value['@container'].inspect}"}
686
- definition.container_mapping = check_container(value['@container'], local_context, defined, term)
851
+ container = value['@container']
852
+ raise JsonLdError::InvalidReverseProperty,
853
+ "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" unless
854
+ container.is_a?(String) && (container == '@set' || container == '@index')
855
+ definition.container_mapping = check_container(container, local_context, defined, term)
856
+ end
857
+ definition.reverse_property = true
858
+ elsif value.has_key?('@id') && value['@id'].nil?
859
+ # Allowed to reserve a null term, which may be protected
860
+ elsif value.has_key?('@id') && value['@id'] != term
861
+ raise JsonLdError::InvalidIRIMapping, "expected value of @id to be a string: #{value['@id'].inspect} on term #{term.inspect}" unless
862
+ value['@id'].is_a?(String)
863
+
864
+ if !KEYWORDS.include?(value['@id'].to_s) && value['@id'].to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
865
+ warn "Values beginning with '@' are reserved for future use and ignored: #{value['@id']}."
866
+ return
687
867
  end
688
868
 
689
- if value.has_key?('@context')
690
- begin
691
- self.parse(value['@context'])
692
- definition.context = value['@context']
693
- rescue JsonLdError => e
694
- raise JsonLdError::InvalidScopedContext, "Term definition for #{term.inspect} contains illegal value for @context: #{e.message}"
869
+ definition.id = expand_iri(value['@id'],
870
+ vocab: true,
871
+ local_context: local_context,
872
+ defined: defined)
873
+ raise JsonLdError::InvalidKeywordAlias, "expected value of @id to not be @context on term #{term.inspect}" if
874
+ definition.id == '@context'
875
+
876
+ if term.match?(/(?::[^:])|\//)
877
+ term_iri = expand_iri(term,
878
+ vocab: true,
879
+ local_context: local_context,
880
+ defined: defined.merge(term => true))
881
+ if term_iri != definition.id
882
+ raise JsonLdError::InvalidIRIMapping, "term #{term} expands to #{definition.id}, not #{term_iri}"
695
883
  end
696
884
  end
697
885
 
698
- if value.has_key?('@language')
699
- language = value['@language']
700
- raise JsonLdError::InvalidLanguageMapping, "language must be null or a string, was #{language.inspect}} on term #{term.inspect}" unless language.nil? || (language || "").is_a?(String)
701
- language = language.downcase if language.is_a?(String)
702
- #log_debug("") {"language_mapping: #{language.inspect}"}
703
- definition.language_mapping = language || false
886
+ warn "[DEPRECATION] Blank Node terms deprecated in JSON-LD 1.1." if @options[:validate] && processingMode('json-ld-1.1') && definition.id.to_s.start_with?("_:")
887
+
888
+ # If id ends with a gen-delim, it may be used as a prefix for simple terms
889
+ definition.prefix = true if !term.include?(':') &&
890
+ simple_term &&
891
+ (definition.id.to_s.end_with?(':', '/', '?', '#', '[', ']', '@') || definition.id.to_s.start_with?('_:'))
892
+ elsif term[1..-1].include?(':')
893
+ # If term is a compact IRI with a prefix that is a key in local context then a dependency has been found. Use this algorithm recursively passing active context, local context, the prefix as term, and defined.
894
+ prefix, suffix = term.split(':', 2)
895
+ create_term_definition(local_context, prefix, defined, protected: protected) if local_context.has_key?(prefix)
896
+
897
+ definition.id = if td = term_definitions[prefix]
898
+ # If term's prefix has a term definition in active context, set the IRI mapping for definition to the result of concatenating the value associated with the prefix's IRI mapping and the term's suffix.
899
+ td.id + suffix
900
+ else
901
+ # Otherwise, term is an absolute IRI. Set the IRI mapping for definition to term
902
+ term
903
+ end
904
+ #log_debug("") {"=> #{definition.id}"}
905
+ elsif term.include?('/')
906
+ # If term is a relative IRI
907
+ definition.id = expand_iri(term, vocab: true)
908
+ raise JsonLdError::InvalidKeywordAlias, "expected term to expand to an absolute IRI #{term.inspect}" unless
909
+ definition.id.absolute?
910
+ elsif KEYWORDS.include?(term)
911
+ # This should only happen for @type when @container is @set
912
+ definition.id = term
913
+ else
914
+ # Otherwise, active context must have a vocabulary mapping, otherwise an invalid value has been detected, which is an error. Set the IRI mapping for definition to the result of concatenating the value associated with the vocabulary mapping and term.
915
+ raise JsonLdError::InvalidIRIMapping, "relative term definition without vocab: #{term} on term #{term.inspect}" unless vocab
916
+ definition.id = vocab + term
917
+ #log_debug("") {"=> #{definition.id}"}
918
+ end
919
+
920
+ @iri_to_term[definition.id] = term if simple_term && definition.id
921
+
922
+ if value.has_key?('@container')
923
+ #log_debug("") {"container_mapping: #{value['@container'].inspect}"}
924
+ definition.container_mapping = check_container(value['@container'], local_context, defined, term)
925
+
926
+ # If @container includes @type
927
+ if definition.container_mapping.include?('@type')
928
+ # If definition does not have @type, set @type to @id
929
+ definition.type_mapping ||= '@id'
930
+ # If definition includes @type with a value other than @id or @vocab, an illegal type mapping error has been detected
931
+ if !CONTEXT_TYPE_ID_VOCAB.include?(definition.type_mapping)
932
+ raise JsonLdError::InvalidTypeMapping, "@container: @type requires @type to be @id or @vocab"
933
+ end
704
934
  end
935
+ end
705
936
 
706
- if value.has_key?('@nest')
707
- nest = value['@nest']
708
- raise JsonLdError::InvalidNestValue, "nest must be a string, was #{nest.inspect}} on term #{term.inspect}" unless nest.is_a?(String)
709
- raise JsonLdError::InvalidNestValue, "nest must not be a keyword other than @nest, was #{nest.inspect}} on term #{term.inspect}" if nest.start_with?('@') && nest != '@nest'
710
- #log_debug("") {"nest: #{nest.inspect}"}
711
- definition.nest = nest
937
+ if value.has_key?('@index')
938
+ # property-based indexing
939
+ raise JsonLdError::InvalidTermDefinition, "@index without @index in @container: #{value['@index']} on term #{term.inspect}" unless definition.container_mapping.include?('@index')
940
+ raise JsonLdError::InvalidTermDefinition, "@index must expand to an IRI: #{value['@index']} on term #{term.inspect}" unless value['@index'].is_a?(String) && !value['@index'].start_with?('@')
941
+ definition.index = value['@index'].to_s
942
+ end
943
+
944
+ if value.has_key?('@context')
945
+ begin
946
+ self.parse(value['@context'], override_protected: true)
947
+ # Record null context in array form
948
+ definition.context = value['@context'] ? value['@context'] : [nil]
949
+ rescue JsonLdError => e
950
+ raise JsonLdError::InvalidScopedContext, "Term definition for #{term.inspect} contains illegal value for @context: #{e.message}"
712
951
  end
952
+ end
713
953
 
714
- if value.has_key?('@prefix')
715
- raise JsonLdError::InvalidTermDefinition, "@prefix used on compact IRI term #{term.inspect}" if term.include?(':')
716
- case pfx = value['@prefix']
717
- when TrueClass, FalseClass
718
- definition.prefix = pfx
719
- else
720
- raise JsonLdError::InvalidPrefixValue, "unknown value for '@prefix': #{pfx.inspect} on term #{term.inspect}"
954
+ if value.has_key?('@language')
955
+ language = value['@language']
956
+ language = case value['@language']
957
+ when String
958
+ # Warn on an invalid language tag, unless :validate is true, in which case it's an error
959
+ if value['@language'] !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
960
+ warn "@language must be valid BCP47: #{value['@language'].inspect}"
721
961
  end
962
+ options[:lowercaseLanguage] ? value['@language'].downcase : value['@language']
963
+ when nil
964
+ nil
965
+ else
966
+ raise JsonLdError::InvalidLanguageMapping, "language must be null or a string, was #{value['@language'].inspect}} on term #{term.inspect}"
722
967
  end
968
+ #log_debug("") {"language_mapping: #{language.inspect}"}
969
+ definition.language_mapping = language || false
970
+ end
723
971
 
724
- term_definitions[term] = definition
725
- defined[term] = true
726
- else
727
- raise JsonLdError::InvalidTermDefinition, "Term definition for #{term.inspect} is an #{value.class} on term #{term.inspect}"
972
+ if value.has_key?('@direction')
973
+ direction = value['@direction']
974
+ raise JsonLdError::InvalidBaseDirection, "direction must be null, 'ltr', or 'rtl', was #{language.inspect}} on term #{term.inspect}" unless direction.nil? || %w(ltr rtl).include?(direction)
975
+ #log_debug("") {"direction_mapping: #{direction.inspect}"}
976
+ definition.direction_mapping = direction || false
977
+ end
978
+
979
+ if value.has_key?('@nest')
980
+ nest = value['@nest']
981
+ raise JsonLdError::InvalidNestValue, "nest must be a string, was #{nest.inspect}} on term #{term.inspect}" unless nest.is_a?(String)
982
+ raise JsonLdError::InvalidNestValue, "nest must not be a keyword other than @nest, was #{nest.inspect}} on term #{term.inspect}" if nest.match?(/^@[a-zA-Z]+$/) && nest != '@nest'
983
+ #log_debug("") {"nest: #{nest.inspect}"}
984
+ definition.nest = nest
985
+ end
986
+
987
+ if value.has_key?('@prefix')
988
+ raise JsonLdError::InvalidTermDefinition, "@prefix used on compact or relative IRI term #{term.inspect}" if term.match?(%r{:|/})
989
+ case pfx = value['@prefix']
990
+ when TrueClass, FalseClass
991
+ definition.prefix = pfx
992
+ else
993
+ raise JsonLdError::InvalidPrefixValue, "unknown value for '@prefix': #{pfx.inspect} on term #{term.inspect}"
994
+ end
995
+
996
+ raise JsonLdError::InvalidTermDefinition, "keywords may not be used as prefixes" if pfx && KEYWORDS.include?(definition.id.to_s)
728
997
  end
998
+
999
+ if previous_definition && previous_definition.protected? && definition != previous_definition && !override_protected
1000
+ definition = previous_definition
1001
+ raise JSON::LD::JsonLdError::ProtectedTermRedefinition, "Attempt to redefine protected term #{term}"
1002
+ end
1003
+
1004
+ term_definitions[term] = definition
1005
+ defined[term] = true
729
1006
  ensure
730
1007
  # Re-build after term definitions set
731
1008
  @inverse_context = nil
@@ -753,6 +1030,7 @@ module JSON::LD
753
1030
  #log_debug("") {"=> context: #{inspect}"}
754
1031
  ctx = {}
755
1032
  ctx['@base'] = base.to_s if base && base != doc_base
1033
+ ctx['@direction'] = default_direction.to_s if default_direction
756
1034
  ctx['@language'] = default_language.to_s if default_language
757
1035
  ctx['@vocab'] = vocab.to_s if vocab
758
1036
 
@@ -808,7 +1086,7 @@ module JSON::LD
808
1086
  statements.each do |subject, values|
809
1087
  types = values.each_with_object([]) { |v, memo| memo << v.object if v.predicate == RDF.type }
810
1088
  is_property = types.any? {|t| t.to_s.include?("Property")}
811
-
1089
+
812
1090
  term = subject.to_s.split(/[\/\#]/).last
813
1091
 
814
1092
  if !is_property
@@ -831,6 +1109,7 @@ module JSON::LD
831
1109
  if self.default_language
832
1110
  td.language_mapping = false
833
1111
  end
1112
+ # FIXME: text direction
834
1113
  when RDF::XSD.boolean, RDF::SCHEMA.Boolean, RDF::XSD.date, RDF::SCHEMA.Date,
835
1114
  RDF::XSD.dateTime, RDF::SCHEMA.DateTime, RDF::XSD.time, RDF::SCHEMA.Time,
836
1115
  RDF::XSD.duration, RDF::SCHEMA.Duration, RDF::XSD.decimal, RDF::SCHEMA.Number,
@@ -856,7 +1135,7 @@ module JSON::LD
856
1135
  def set_mapping(term, value)
857
1136
  #log_debug("") {"map #{term.inspect} to #{value.inspect}"}
858
1137
  term = term.to_s
859
- term_definitions[term] = TermDefinition.new(term, id: value, simple: true, prefix: (value.to_s.end_with?(*%w(: / ? # [ ] @))))
1138
+ term_definitions[term] = TermDefinition.new(term, id: value, simple: true, prefix: (value.to_s.end_with?(*PREFIX_URI_ENDINGS)))
860
1139
  term_definitions[term].simple = true
861
1140
 
862
1141
  term_sym = term.empty? ? "" : term.to_sym
@@ -881,11 +1160,24 @@ module JSON::LD
881
1160
  # @param [Term, #to_s] term in unexpanded form
882
1161
  # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>]
883
1162
  def container(term)
884
- return [term] if KEYWORDS.include?(term)
1163
+ return [term] if term == '@list'
885
1164
  term = find_definition(term)
886
1165
  term ? term.container_mapping : []
887
1166
  end
888
1167
 
1168
+ ##
1169
+ # Retrieve term coercion
1170
+ #
1171
+ # @param [Term, #to_s] term in unexpanded form
1172
+ # @return [RDF::URI, '@id']
1173
+ def coerce(term)
1174
+ # Map property, if it's not an RDF::Value
1175
+ # @type is always is an IRI
1176
+ return '@id' if term == RDF.type || term == '@type'
1177
+ term = find_definition(term)
1178
+ term && term.type_mapping
1179
+ end
1180
+
889
1181
  ##
890
1182
  # Should values be represented using an array?
891
1183
  #
@@ -935,7 +1227,17 @@ module JSON::LD
935
1227
  def language(term)
936
1228
  term = find_definition(term)
937
1229
  lang = term && term.language_mapping
938
- lang.nil? ? @default_language : lang
1230
+ lang.nil? ? @default_language : (lang == false ? nil : lang)
1231
+ end
1232
+
1233
+ ##
1234
+ # Retrieve the text direction associated with a term, or the default direction otherwise
1235
+ # @param [Term, #to_s] term in unexpanded form
1236
+ # @return [String]
1237
+ def direction(term)
1238
+ term = find_definition(term)
1239
+ dir = term && term.direction_mapping
1240
+ dir.nil? ? @default_direction : (dir == false ? nil : dir)
939
1241
  end
940
1242
 
941
1243
  ##
@@ -984,11 +1286,12 @@ module JSON::LD
984
1286
  # @return [RDF::URI, String]
985
1287
  # IRI or String, if it's a keyword
986
1288
  # @raise [JSON::LD::JsonLdError::InvalidIRIMapping] if the value cannot be expanded
987
- # @see http://json-ld.org/spec/latest/json-ld-api/#iri-expansion
1289
+ # @see https://www.w3.org/TR/json-ld11-api/#iri-expansion
988
1290
  def expand_iri(value, documentRelative: false, vocab: false, local_context: nil, defined: nil, quiet: false, **options)
989
1291
  return value unless value.is_a?(String)
990
1292
 
991
1293
  return value if KEYWORDS.include?(value)
1294
+ return nil if value.match?(/^@[a-zA-Z]+$/)
992
1295
  #log_debug("expand_iri") {"value: #{value.inspect}"} unless quiet
993
1296
 
994
1297
  defined = defined || {} # if we initialized in the keyword arg we would allocate {} at each invokation, even in the 2 (common) early returns above.
@@ -1011,7 +1314,7 @@ module JSON::LD
1011
1314
  end
1012
1315
 
1013
1316
  # If value contains a colon (:), it is either an absolute IRI or a compact IRI:
1014
- if value.include?(':')
1317
+ if value[1..-1].to_s.include?(':')
1015
1318
  prefix, suffix = value.split(':', 2)
1016
1319
  #log_debug("") {"prefix: #{prefix.inspect}, suffix: #{suffix.inspect}, vocab: #{self.vocab.inspect}"} unless quiet
1017
1320
 
@@ -1025,22 +1328,21 @@ module JSON::LD
1025
1328
  end
1026
1329
 
1027
1330
  # If active context contains a term definition for prefix, return the result of concatenating the IRI mapping associated with prefix and suffix.
1028
- result = if (td = term_definitions[prefix])
1029
- result = td.id + suffix
1331
+ if (td = term_definitions[prefix]) && !td.id.nil? && td.prefix?
1332
+ return td.id + suffix
1333
+ elsif RDF::URI(value).absolute?
1334
+ # Otherwise, if the value has the form of an absolute IRI, return it
1335
+ return RDF::URI(value)
1030
1336
  else
1031
- # (Otherwise) Return value as it is already an absolute IRI.
1032
- RDF::URI(value)
1337
+ # Otherwise, it is a relative IRI
1033
1338
  end
1034
-
1035
- #log_debug("") {"=> #{result.inspect}"} unless quiet
1036
- return result
1037
1339
  end
1038
1340
  #log_debug("") {"=> #{result.inspect}"} unless quiet
1039
1341
 
1040
1342
  result = if vocab && self.vocab
1041
1343
  # If vocab is true, and active context has a vocabulary mapping, return the result of concatenating the vocabulary mapping with value.
1042
1344
  self.vocab + value
1043
- elsif (documentRelative || self.vocab == '') && (base ||= self.base)
1345
+ elsif documentRelative && (base ||= self.base)
1044
1346
  # Otherwise, if document relative is true, set value to the result of resolving value against the base IRI. Only the basic algorithm in section 5.2 of [RFC3986] is used; neither Syntax-Based Normalization nor Scheme-Based Normalization are performed. Characters additionally allowed in IRI references are treated in the same way that unreserved characters are treated in URI references, per section 6.5 of [RFC3987].
1045
1347
  value = RDF::URI(value)
1046
1348
  value.absolute? ? value : RDF::URI(base).join(value)
@@ -1054,6 +1356,18 @@ module JSON::LD
1054
1356
  result
1055
1357
  end
1056
1358
 
1359
+ # The following constants are used to reduce object allocations in #compact_iri below
1360
+ CONTAINERS_GRAPH = %w(@graph@id @graph@id@set).freeze
1361
+ CONTAINERS_GRAPH_INDEX = %w(@graph@index @graph@index@set).freeze
1362
+ CONTAINERS_GRAPH_INDEX_INDEX = %w(@graph@index @graph@index@set @index @index@set).freeze
1363
+ CONTAINERS_GRAPH_SET = %w(@graph @graph@set @set).freeze
1364
+ CONTAINERS_ID_TYPE = %w(@id @id@set @type @set@type).freeze
1365
+ CONTAINERS_ID_VOCAB = %w(@id @vocab @none).freeze
1366
+ CONTAINERS_INDEX_SET = %w(@index @index@set).freeze
1367
+ CONTAINERS_LANGUAGE = %w(@language @language@set).freeze
1368
+ CONTAINERS_VALUE = %w(@value).freeze
1369
+ CONTAINERS_VOCAB_ID = %w(@vocab @id @none).freeze
1370
+
1057
1371
  ##
1058
1372
  # Compacts an absolute IRI to the shortest matching term or compact IRI
1059
1373
  #
@@ -1063,12 +1377,12 @@ module JSON::LD
1063
1377
  # @param [Boolean] vocab
1064
1378
  # specifies whether the passed iri should be compacted using the active context's vocabulary mapping
1065
1379
  # @param [Boolean] reverse
1066
- # specifies whether a reverse property is being compacted
1380
+ # specifies whether a reverse property is being compacted
1067
1381
  # @param [Boolean] quiet (false)
1068
1382
  # @param [Hash{Symbol => Object}] options ({})
1069
1383
  #
1070
1384
  # @return [String] compacted form of IRI
1071
- # @see http://json-ld.org/spec/latest/json-ld-api/#iri-compaction
1385
+ # @see https://www.w3.org/TR/json-ld11-api/#iri-compaction
1072
1386
  def compact_iri(iri, value: nil, vocab: nil, reverse: false, quiet: false, **options)
1073
1387
  return if iri.nil?
1074
1388
  iri = iri.to_s
@@ -1076,10 +1390,14 @@ module JSON::LD
1076
1390
 
1077
1391
  if vocab && inverse_context.has_key?(iri)
1078
1392
  #log_debug("") {"vocab and key in inverse context"} unless quiet
1079
- default_language = self.default_language || "@none"
1393
+ default_language = if self.default_direction
1394
+ "#{self.default_language}_#{self.default_direction}".downcase
1395
+ else
1396
+ (self.default_language || "@none").downcase
1397
+ end
1080
1398
  containers = []
1081
1399
  tl, tl_value = "@language", "@null"
1082
- containers.concat(%w(@index @index@set)) if index?(value) && !graph?(value)
1400
+ containers.concat(CONTAINERS_INDEX_SET) if index?(value) && !graph?(value)
1083
1401
 
1084
1402
  # If the value is a JSON Object with the key @preserve, use the value of @preserve.
1085
1403
  value = value['@preserve'].first if value.is_a?(Hash) && value.has_key?('@preserve')
@@ -1097,8 +1415,10 @@ module JSON::LD
1097
1415
  list.each do |item|
1098
1416
  item_language, item_type = "@none", "@none"
1099
1417
  if value?(item)
1100
- if item.has_key?('@language')
1101
- item_language = item['@language']
1418
+ if item.has_key?('@direction')
1419
+ item_language = "#{item['@language']}_#{item['@direction']}".downcase
1420
+ elsif item.has_key?('@language')
1421
+ item_language = item['@language'].downcase
1102
1422
  elsif item.has_key?('@type')
1103
1423
  item_type = item['@type']
1104
1424
  else
@@ -1130,29 +1450,35 @@ module JSON::LD
1130
1450
  #log_debug("") {"list: containers: #{containers.inspect}, type/language: #{tl.inspect}, type/language value: #{tl_value.inspect}"} unless quiet
1131
1451
  elsif graph?(value)
1132
1452
  # Prefer @index and @id containers, then @graph, then @index
1133
- containers.concat(%w(@graph@index @graph@index@set @index @index@set)) if index?(value)
1134
- containers.concat(%w(@graph@id @graph@id@set)) if value.has_key?('@id')
1453
+ containers.concat(CONTAINERS_GRAPH_INDEX_INDEX) if index?(value)
1454
+ containers.concat(CONTAINERS_GRAPH) if value.has_key?('@id')
1135
1455
 
1136
1456
  # Prefer an @graph container next
1137
- containers.concat(%w(@graph @graph@set @set))
1457
+ containers.concat(CONTAINERS_GRAPH_SET)
1138
1458
 
1139
1459
  # Lastly, in 1.1, any graph can be indexed on @index or @id, so add if we haven't already
1140
- containers.concat(%w(@graph@index @graph@index@set)) unless index?(value)
1141
- containers.concat(%w(@graph@id @graph@id@set)) unless value.has_key?('@id')
1142
- containers.concat(%w(@index @index@set)) unless index?(value)
1460
+ containers.concat(CONTAINERS_GRAPH_INDEX) unless index?(value)
1461
+ containers.concat(CONTAINERS_GRAPH) unless value.has_key?('@id')
1462
+ containers.concat(CONTAINERS_INDEX_SET) unless index?(value)
1463
+ containers << '@set'
1464
+
1465
+ tl, tl_value = '@type', '@id'
1143
1466
  else
1144
1467
  if value?(value)
1145
1468
  # In 1.1, an language map can be used to index values using @none
1146
1469
  if value.has_key?('@language') && !index?(value)
1147
- tl_value = value['@language']
1148
- containers.concat(%w(@language @language@set))
1470
+ tl_value = value['@language'].downcase
1471
+ tl_value += "_#{value['@direction']}" if value['@direction']
1472
+ containers.concat(CONTAINERS_LANGUAGE)
1473
+ elsif value.has_key?('@direction') && !index?(value)
1474
+ tl_value = "_#{value['@direction']}"
1149
1475
  elsif value.has_key?('@type')
1150
1476
  tl_value = value['@type']
1151
1477
  tl = '@type'
1152
1478
  end
1153
1479
  else
1154
1480
  # In 1.1, an id or type map can be used to index values using @none
1155
- containers.concat(%w(@id @id@set @type @set@type))
1481
+ containers.concat(CONTAINERS_ID_TYPE)
1156
1482
  tl, tl_value = '@type', '@id'
1157
1483
  end
1158
1484
  containers << '@set'
@@ -1162,9 +1488,9 @@ module JSON::LD
1162
1488
  containers << '@none'
1163
1489
 
1164
1490
  # In 1.1, an index map can be used to index values using @none, so add as a low priority
1165
- containers.concat(%w(@index @index@set)) unless index?(value)
1491
+ containers.concat(CONTAINERS_INDEX_SET) unless index?(value)
1166
1492
  # Values without type or language can use @language map
1167
- containers.concat(%w(@language @language@set)) if value?(value) && value.keys == %w(@value)
1493
+ containers.concat(CONTAINERS_LANGUAGE) if value?(value) && value.keys == CONTAINERS_VALUE
1168
1494
 
1169
1495
  tl_value ||= '@null'
1170
1496
  preferred_values = []
@@ -1172,15 +1498,22 @@ module JSON::LD
1172
1498
  if (tl_value == '@id' || tl_value == '@reverse') && value.is_a?(Hash) && value.has_key?('@id')
1173
1499
  t_iri = compact_iri(value['@id'], vocab: true, document_relative: true)
1174
1500
  if (r_td = term_definitions[t_iri]) && r_td.id == value['@id']
1175
- preferred_values.concat(%w(@vocab @id @none))
1501
+ preferred_values.concat(CONTAINERS_VOCAB_ID)
1176
1502
  else
1177
- preferred_values.concat(%w(@id @vocab @none))
1503
+ preferred_values.concat(CONTAINERS_ID_VOCAB)
1178
1504
  end
1179
1505
  else
1180
1506
  tl = '@any' if list?(value) && value['@list'].empty?
1181
1507
  preferred_values.concat([tl_value, '@none'].compact)
1182
1508
  end
1183
1509
  #log_debug("") {"preferred_values: #{preferred_values.inspect}"} unless quiet
1510
+ preferred_values << '@any'
1511
+
1512
+ # if containers included `@language` and preferred_values includes something of the form language-tag_direction, add just the _direction part, to select terms that have that direction.
1513
+ if lang_dir = preferred_values.detect {|v| v.include?('_')}
1514
+ preferred_values << '_' + lang_dir.split('_').last
1515
+ end
1516
+
1184
1517
  if p_term = select_term(iri, containers, tl, preferred_values)
1185
1518
  #log_debug("") {"=> term: #{p_term.inspect}"} unless quiet
1186
1519
  return p_term
@@ -1224,6 +1557,12 @@ module JSON::LD
1224
1557
  return candidates.sort.first if !candidates.empty?
1225
1558
  end
1226
1559
 
1560
+ # If iri could be confused with a compact IRI using a term in this context, signal an error
1561
+ term_definitions.each do |term, td|
1562
+ next unless iri.to_s.start_with?("#{term}:") && td.prefix?
1563
+ raise JSON::LD::JsonLdError:: IRIConfusedWithPrefix, "Absolute IRI '#{iri}' confused with prefix '#{term}'"
1564
+ end
1565
+
1227
1566
  if !vocab
1228
1567
  # transform iri to a relative IRI using the document's base IRI
1229
1568
  iri = remove_base(iri)
@@ -1247,12 +1586,13 @@ module JSON::LD
1247
1586
  # @param [Hash, String] value
1248
1587
  # Value (literal or IRI) to be expanded
1249
1588
  # @param [Boolean] useNativeTypes (false) use native representations
1589
+ # @param [Boolean] rdfDirection (nil) decode i18n datatype if i18n-datatype
1250
1590
  # @param [Hash{Symbol => Object}] options
1251
1591
  #
1252
1592
  # @return [Hash] Object representation of value
1253
1593
  # @raise [RDF::ReaderError] if the iri cannot be expanded
1254
- # @see http://json-ld.org/spec/latest/json-ld-api/#value-expansion
1255
- def expand_value(property, value, useNativeTypes: false, **options)
1594
+ # @see https://www.w3.org/TR/json-ld11-api/#value-expansion
1595
+ def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, **options)
1256
1596
  #log_debug("expand_value") {"property: #{property.inspect}, value: #{value.inspect}"}
1257
1597
 
1258
1598
  td = term_definitions.fetch(property, TermDefinition.new(property))
@@ -1281,7 +1621,26 @@ module JSON::LD
1281
1621
  when RDF::Literal
1282
1622
  #log_debug("Literal") {"datatype: #{value.datatype.inspect}"}
1283
1623
  res = {}
1284
- if useNativeTypes && RDF_LITERAL_NATIVE_TYPES.include?(value.datatype)
1624
+ if value.datatype == RDF::URI(RDF.to_uri + "JSON") && processingMode('json-ld-1.1')
1625
+ # Value parsed as JSON
1626
+ # FIXME: MultiJson
1627
+ res['@value'] = ::JSON.parse(value.object)
1628
+ res['@type'] = '@json'
1629
+ elsif value.datatype.start_with?("https://www.w3.org/ns/i18n#") && rdfDirection == 'i18n-datatype' && processingMode('json-ld-1.1')
1630
+ lang, dir = value.datatype.fragment.split('_')
1631
+ res['@value'] = value.to_s
1632
+ unless lang.empty?
1633
+ if lang !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
1634
+ if options[:validate]
1635
+ raise JsonLdError::InvalidLanguageMapping, "rdf:language must be valid BCP47: #{lang.inspect}"
1636
+ else
1637
+ warn "rdf:language must be valid BCP47: #{lang.inspect}"
1638
+ end
1639
+ end
1640
+ res['@language'] = lang
1641
+ end
1642
+ res['@direction'] = dir
1643
+ elsif useNativeTypes && RDF_LITERAL_NATIVE_TYPES.include?(value.datatype)
1285
1644
  res['@value'] = value.object
1286
1645
  res['@type'] = uri(coerce(property)) if coerce(property)
1287
1646
  else
@@ -1293,6 +1652,7 @@ module JSON::LD
1293
1652
  res['@type'] = uri(value.datatype).to_s
1294
1653
  elsif value.has_language? || language(property)
1295
1654
  res['@language'] = (value.language || language(property)).to_s
1655
+ # FIXME: direction
1296
1656
  end
1297
1657
  end
1298
1658
  res
@@ -1300,12 +1660,13 @@ module JSON::LD
1300
1660
  # Otherwise, initialize result to a JSON object with an @value member whose value is set to value.
1301
1661
  res = {'@value' => value}
1302
1662
 
1303
- if td.type_mapping && !%w(@id @vocab).include?(td.type_mapping.to_s)
1663
+ if td.type_mapping && !CONTAINERS_ID_VOCAB.include?(td.type_mapping.to_s)
1304
1664
  res['@type'] = td.type_mapping.to_s
1305
- elsif value.is_a?(String) && td.language_mapping
1306
- res['@language'] = td.language_mapping
1307
- elsif value.is_a?(String) && default_language && td.language_mapping.nil?
1308
- res['@language'] = default_language
1665
+ elsif value.is_a?(String)
1666
+ language = language(property)
1667
+ direction = direction(property)
1668
+ res['@language'] = language if language
1669
+ res['@direction'] = direction if direction
1309
1670
  end
1310
1671
 
1311
1672
  res
@@ -1313,6 +1674,8 @@ module JSON::LD
1313
1674
 
1314
1675
  #log_debug("") {"=> #{result.inspect}"}
1315
1676
  result
1677
+ rescue ::JSON::ParserError => e
1678
+ raise JSON::LD::JsonLdError::InvalidJsonLiteral, e.message
1316
1679
  end
1317
1680
 
1318
1681
  ##
@@ -1326,25 +1689,21 @@ module JSON::LD
1326
1689
  #
1327
1690
  # @return [Hash] Object representation of value
1328
1691
  # @raise [JsonLdError] if the iri cannot be expanded
1329
- # @see http://json-ld.org/spec/latest/json-ld-api/#value-compaction
1692
+ # @see https://www.w3.org/TR/json-ld11-api/#value-compaction
1330
1693
  # FIXME: revisit the specification version of this.
1331
1694
  def compact_value(property, value, **options)
1332
1695
  #log_debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}"}
1333
1696
 
1334
- num_members = value.length
1335
-
1336
- num_members -= 1 if index?(value) && container(property).include?('@index')
1337
- if num_members > 2
1338
- #log_debug("") {"can't compact value with # members > 2"}
1339
- return value
1340
- end
1697
+ indexing = index?(value) && container(property).include?('@index')
1698
+ language = language(property)
1699
+ direction = direction(property)
1341
1700
 
1342
1701
  result = case
1343
- when coerce(property) == '@id' && value.has_key?('@id') && num_members == 1
1702
+ when coerce(property) == '@id' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty?
1344
1703
  # Compact an @id coercion
1345
1704
  #log_debug("") {" (@id & coerce)"}
1346
1705
  compact_iri(value['@id'])
1347
- when coerce(property) == '@vocab' && value.has_key?('@id') && num_members == 1
1706
+ when coerce(property) == '@vocab' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty?
1348
1707
  # Compact an @id coercion
1349
1708
  #log_debug("") {" (@id & coerce & vocab)"}
1350
1709
  compact_iri(value['@id'], vocab: true)
@@ -1352,25 +1711,32 @@ module JSON::LD
1352
1711
  #log_debug("") {" (@id)"}
1353
1712
  # return value as is
1354
1713
  value
1355
- when value['@type'] && expand_iri(value['@type'], vocab: true) == coerce(property)
1714
+ when value['@type'] && value['@type'] == coerce(property)
1356
1715
  # Compact common datatype
1357
1716
  #log_debug("") {" (@type & coerce) == #{coerce(property)}"}
1358
1717
  value['@value']
1359
- when value['@language'] && (value['@language'] == language(property))
1360
- # Compact language
1361
- #log_debug("") {" (@language) == #{language(property).inspect}"}
1362
- value['@value']
1363
- when num_members == 1 && !value['@value'].is_a?(String)
1718
+ when coerce(property) == '@none' || value['@type']
1719
+ # use original expanded value
1720
+ value
1721
+ when !value['@value'].is_a?(String)
1364
1722
  #log_debug("") {" (native)"}
1365
- value['@value']
1366
- when num_members == 1 && default_language.nil? || language(property) == false
1367
- #log_debug("") {" (!@language)"}
1368
- value['@value']
1723
+ indexing || !index?(value) ? value['@value'] : value
1724
+ when value['@language'].to_s.downcase == language.to_s.downcase && value['@direction'] == direction
1725
+ # Compact language and direction
1726
+ indexing || !index?(value) ? value['@value'] : value
1369
1727
  else
1370
- # Otherwise, use original value
1371
- #log_debug("") {" (no change)"}
1372
1728
  value
1373
1729
  end
1730
+
1731
+ if result.is_a?(Hash) && result.has_key?('@type') && value['@type'] != '@json'
1732
+ # Compact values of @type
1733
+ c_type = if result['@type'].is_a?(Array)
1734
+ result['@type'].map {|t| compact_iri(t, vocab: true)}
1735
+ else
1736
+ compact_iri(result['@type'], vocab: true)
1737
+ end
1738
+ result = result.merge('@type' => c_type)
1739
+ end
1374
1740
 
1375
1741
  # If the result is an object, tranform keys using any term keyword aliases
1376
1742
  if result.is_a?(Hash) && result.keys.any? {|k| self.alias(k) != k}
@@ -1388,8 +1754,11 @@ module JSON::LD
1388
1754
 
1389
1755
  ##
1390
1756
  # Turn this into a source for a new instantiation
1757
+ # @param [Array<String>] aliases
1758
+ # Other URLs to alias when preloading
1391
1759
  # @return [String]
1392
- def to_rb
1760
+ def to_rb(*aliases)
1761
+ canon_base = RDF::URI(context_base).canonicalize
1393
1762
  defn = []
1394
1763
 
1395
1764
  defn << "base: #{self.base.to_s.inspect}" if self.base
@@ -1406,7 +1775,9 @@ module JSON::LD
1406
1775
  require 'json/ld'
1407
1776
  class JSON::LD::Context
1408
1777
  ).gsub(/^ /, '') +
1409
- " add_preloaded(#{RDF::URI(context_base).canonicalize.to_s.inspect}) do\n new(" + defn.join(", ") + ")\n end\nend\n"
1778
+ %[ add_preloaded("#{canon_base}") do\n new(] + defn.join(", ") + ")\n end\n" +
1779
+ aliases.map {|a| %[ alias_preloaded("#{a}", "#{canon_base}")\n]}.join("") +
1780
+ "end\n"
1410
1781
  end
1411
1782
 
1412
1783
  def inspect
@@ -1415,10 +1786,12 @@ module JSON::LD
1415
1786
  v << "vocab=#{vocab}" if vocab
1416
1787
  v << "processingMode=#{processingMode}" if processingMode
1417
1788
  v << "default_language=#{default_language}" if default_language
1789
+ v << "default_direction=#{default_direction}" if default_direction
1790
+ v << "previous_context" if previous_context
1418
1791
  v << "term_definitions[#{term_definitions.length}]=#{term_definitions}"
1419
1792
  v.join(" ") + "]"
1420
1793
  end
1421
-
1794
+
1422
1795
  def dup
1423
1796
  # Also duplicate mappings, coerce and list
1424
1797
  that = self
@@ -1432,19 +1805,6 @@ module JSON::LD
1432
1805
 
1433
1806
  protected
1434
1807
 
1435
- ##
1436
- # Retrieve term coercion
1437
- #
1438
- # @param [String] property in unexpanded form
1439
- #
1440
- # @return [RDF::URI, '@id']
1441
- def coerce(property)
1442
- # Map property, if it's not an RDF::Value
1443
- # @type is always is an IRI
1444
- return '@id' if property == RDF.type || property == '@type'
1445
- term_definitions[property] && term_definitions[property].type_mapping
1446
- end
1447
-
1448
1808
  ##
1449
1809
  # Determine if `term` is a suitable term.
1450
1810
  # Term may be any valid JSON string.
@@ -1473,6 +1833,8 @@ module JSON::LD
1473
1833
  CONTEXT_CONTAINER_ARRAY_TERMS = %w(@set @list @graph).freeze
1474
1834
  CONTEXT_CONTAINER_ID_GRAPH = %w(@id @graph).freeze
1475
1835
  CONTEXT_CONTAINER_INDEX_GRAPH = %w(@index @graph).freeze
1836
+ CONTEXT_BASE_FRAG_OR_QUERY = %w(? #).freeze
1837
+ CONTEXT_TYPE_ID_VOCAB = %w(@id @vocab).freeze
1476
1838
 
1477
1839
  def uri(value)
1478
1840
  case value.to_s
@@ -1520,7 +1882,8 @@ module JSON::LD
1520
1882
  # "@language": {
1521
1883
  # "@null": "term",
1522
1884
  # "@none": "term",
1523
- # "en": "term"
1885
+ # "en": "term",
1886
+ # "ar_rtl": "term"
1524
1887
  # },
1525
1888
  # "@type": {
1526
1889
  # "@reverse": "term",
@@ -1537,7 +1900,7 @@ module JSON::LD
1537
1900
  def inverse_context
1538
1901
  @inverse_context ||= begin
1539
1902
  result = {}
1540
- default_language = self.default_language || '@none'
1903
+ default_language = (self.default_language || '@none').downcase
1541
1904
  term_definitions.keys.sort do |a, b|
1542
1905
  a.length == b.length ? (a <=> b) : (a.length <=> b.length)
1543
1906
  end.each do |term|
@@ -1556,11 +1919,33 @@ module JSON::LD
1556
1919
  any_map['@none'] ||= term
1557
1920
  if td.reverse_property
1558
1921
  type_map['@reverse'] ||= term
1922
+ elsif td.type_mapping == '@none'
1923
+ type_map['@any'] ||= term
1924
+ language_map['@any'] ||= term
1925
+ any_map['@any'] ||= term
1559
1926
  elsif td.type_mapping
1560
1927
  type_map[td.type_mapping.to_s] ||= term
1928
+ elsif !td.language_mapping.nil? && !td.direction_mapping.nil?
1929
+ lang_dir = if td.language_mapping && td.direction_mapping
1930
+ "#{td.language_mapping}_#{td.direction_mapping}".downcase
1931
+ elsif td.language_mapping
1932
+ td.language_mapping.downcase
1933
+ elsif td.direction_mapping
1934
+ "_#{td.direction_mapping}"
1935
+ else
1936
+ "@null"
1937
+ end
1938
+ language_map[lang_dir] ||= term
1561
1939
  elsif !td.language_mapping.nil?
1562
- language = td.language_mapping || '@null'
1563
- language_map[language] ||= term
1940
+ lang_dir = (td.language_mapping || '@null').downcase
1941
+ language_map[lang_dir] ||= term
1942
+ elsif !td.direction_mapping.nil?
1943
+ lang_dir = td.direction_mapping ? "_#{td.direction_mapping}" : '@none'
1944
+ language_map[lang_dir] ||= term
1945
+ elsif default_direction
1946
+ language_map[("#{td.language_mapping}_#{default_direction}").downcase] ||= term
1947
+ language_map['@none'] ||= term
1948
+ type_map['@none'] ||= term
1564
1949
  else
1565
1950
  language_map[default_language] ||= term
1566
1951
  language_map['@none'] ||= term
@@ -1620,7 +2005,7 @@ module JSON::LD
1620
2005
  iri_set
1621
2006
  end
1622
2007
  b = base.to_s
1623
- return iri[b.length..-1] if iri.start_with?(b) && %w(? #).include?(iri[b.length, 1])
2008
+ return iri[b.length..-1] if iri.start_with?(b) && CONTEXT_BASE_FRAG_OR_QUERY.include?(iri[b.length, 1])
1624
2009
 
1625
2010
  @base_and_parents.each_with_index do |bb, index|
1626
2011
  next unless iri.start_with?(bb)
@@ -1668,7 +2053,7 @@ module JSON::LD
1668
2053
  # Ensure @container mapping is appropriate
1669
2054
  # The result is the original container definition. For IRI containers, this is necessary to be able to determine the @type mapping for string values
1670
2055
  def check_container(container, local_context, defined, term)
1671
- if container.is_a?(Array) && (processingMode || 'json-ld-1.0') < 'json-ld-1.1'
2056
+ if container.is_a?(Array) && processingMode('json-ld-1.0')
1672
2057
  raise JsonLdError::InvalidContainerMapping,
1673
2058
  "'@container' on term #{term.inspect} must be a string: #{container.inspect}"
1674
2059
  end
@@ -1684,7 +2069,7 @@ module JSON::LD
1684
2069
  elsif val.include?('@language')
1685
2070
  raise JsonLdError::InvalidContainerMapping,
1686
2071
  "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1687
- has_set && (processingMode || 'json-ld-1.0') < 'json-ld-1.1'
2072
+ has_set && processingMode('json-ld-1.0')
1688
2073
  raise JsonLdError::InvalidContainerMapping,
1689
2074
  "'@container' on term #{term.inspect} using @language cannot have any values other than @set, found #{container.inspect}" unless
1690
2075
  val.length == 1
@@ -1692,7 +2077,7 @@ module JSON::LD
1692
2077
  elsif val.include?('@index')
1693
2078
  raise JsonLdError::InvalidContainerMapping,
1694
2079
  "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1695
- has_set && (processingMode || 'json-ld-1.0') < 'json-ld-1.1'
2080
+ has_set && processingMode('json-ld-1.0')
1696
2081
  raise JsonLdError::InvalidContainerMapping,
1697
2082
  "'@container' on term #{term.inspect} using @index cannot have any values other than @set and/or @graph, found #{container.inspect}" unless
1698
2083
  (val - CONTEXT_CONTAINER_INDEX_GRAPH).empty?
@@ -1700,7 +2085,7 @@ module JSON::LD
1700
2085
  elsif val.include?('@id')
1701
2086
  raise JsonLdError::InvalidContainerMapping,
1702
2087
  "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1703
- (processingMode || 'json-ld-1.0') < 'json-ld-1.1'
2088
+ processingMode('json-ld-1.0')
1704
2089
  raise JsonLdError::InvalidContainerMapping,
1705
2090
  "'@container' on term #{term.inspect} using @id cannot have any values other than @set and/or @graph, found #{container.inspect}" unless
1706
2091
  (val - CONTEXT_CONTAINER_ID_GRAPH).empty?
@@ -1708,7 +2093,7 @@ module JSON::LD
1708
2093
  elsif val.include?('@type') || val.include?('@graph')
1709
2094
  raise JsonLdError::InvalidContainerMapping,
1710
2095
  "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1711
- (processingMode || 'json-ld-1.0') < 'json-ld-1.1'
2096
+ processingMode('json-ld-1.0')
1712
2097
  raise JsonLdError::InvalidContainerMapping,
1713
2098
  "'@container' on term #{term.inspect} using @language cannot have any values other than @set, found #{container.inspect}" unless
1714
2099
  val.length == 1