json-ld 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -8,6 +8,18 @@ module JSON::LD
8
8
  module Expand
9
9
  include Utils
10
10
 
11
+ # The following constant is used to reduce object allocations
12
+ CONTAINER_INDEX_ID_TYPE = Set.new(%w(@index @id @type)).freeze
13
+ CONTAINER_GRAPH_INDEX = %w(@graph @index).freeze
14
+ CONTAINER_INDEX = %w(@index).freeze
15
+ CONTAINER_ID = %w(@id).freeze
16
+ CONTAINER_LIST = %w(@list).freeze
17
+ CONTAINER_TYPE = %w(@type).freeze
18
+ CONTAINER_GRAPH_ID = %w(@graph @id).freeze
19
+ KEYS_VALUE_LANGUAGE_TYPE_INDEX_DIRECTION = %w(@value @language @type @index @direction).freeze
20
+ KEYS_SET_LIST_INDEX = %w(@set @list @index).freeze
21
+ KEYS_INCLUDED_TYPE = %w(@included @type).freeze
22
+
11
23
  ##
12
24
  # Expand an Array or Object given an active context and performing local context expansion.
13
25
  #
@@ -18,17 +30,24 @@ module JSON::LD
18
30
  # Ensure output objects have keys ordered properly
19
31
  # @param [Boolean] framing (false)
20
32
  # Special rules for expanding a frame
33
+ # @param [Boolean] from_map
34
+ # Expanding from a map, which could be an `@type` map, so don't clear out context term definitions
21
35
  # @return [Array<Hash{String => Object}>]
22
- def expand(input, active_property, context, ordered: false, framing: false)
36
+ def expand(input, active_property, context, ordered: false, framing: false, from_map: false)
23
37
  #log_debug("expand") {"input: #{input.inspect}, active_property: #{active_property.inspect}, context: #{context.inspect}"}
24
38
  framing = false if active_property == '@default'
39
+ expanded_active_property = context.expand_iri(active_property, vocab: true).to_s if active_property
40
+
41
+ # Use a term-specific context, if defined, based on the non-type-scoped context.
42
+ property_scoped_context = context.term_definitions[active_property].context if active_property && context.term_definitions[active_property]
43
+
25
44
  result = case input
26
45
  when Array
27
46
  # If element is an array,
28
- is_list = context.container(active_property) == %w(@list)
47
+ is_list = context.container(active_property) == CONTAINER_LIST
29
48
  value = input.each_with_object([]) do |v, memo|
30
49
  # Initialize expanded item to the result of using this algorithm recursively, passing active context, active property, and item as element.
31
- v = expand(v, active_property, context, ordered: ordered, framing: framing)
50
+ v = expand(v, active_property, context, ordered: ordered, framing: framing, from_map: from_map)
32
51
 
33
52
  # If the active property is @list or its container mapping is set to @list and v is an array, change it to a list object
34
53
  v = {"@list" => v} if is_list && v.is_a?(Array)
@@ -42,45 +61,80 @@ module JSON::LD
42
61
 
43
62
  value
44
63
  when Hash
64
+ if context.previous_context
65
+ expanded_key_map = input.keys.inject({}) {|memo, key| memo.merge(key => context.expand_iri(key, vocab: true).to_s)}
66
+ # Revert any previously type-scoped term definitions, unless this is from a map, a value object or a subject reference
67
+ revert_context = !from_map &&
68
+ !expanded_key_map.values.include?('@value') &&
69
+ !(expanded_key_map.values == ['@id'])
70
+
71
+ # If there's a previous context, the context was type-scoped
72
+ context = context.previous_context if revert_context
73
+ end
74
+
75
+ # Apply property-scoped context after reverting term-scoped context
76
+ context = property_scoped_context ? context.parse(property_scoped_context, override_protected: true) : context
77
+
45
78
  # If element contains the key @context, set active context to the result of the Context Processing algorithm, passing active context and the value of the @context key as local context.
46
79
  if input.has_key?('@context')
47
80
  context = context.parse(input.delete('@context'))
48
81
  #log_debug("expand") {"context: #{context.inspect}"}
49
82
  end
50
83
 
84
+ # Set the type-scoped context to the context on input, for use later
85
+ type_scoped_context = context
86
+
51
87
  output_object = {}
52
88
 
53
89
  # See if keys mapping to @type have terms with a local context
54
- input.each_pair do |key, val|
55
- next unless context.expand_iri(key, vocab: true) == '@type'
56
- Array(val).sort.each do |term|
57
- term_context = context.term_definitions[term].context if context.term_definitions[term]
58
- context = term_context ? context.parse(term_context) : context
90
+ type_key = nil
91
+ input.keys.sort.
92
+ select {|k| context.expand_iri(k, vocab: true, quite: true) == '@type'}.
93
+ each do |tk|
94
+
95
+ type_key ||= tk # Side effect saves the first found key mapping to @type
96
+ Array(input[tk]).sort.each do |term|
97
+ term_context = type_scoped_context.term_definitions[term].context if type_scoped_context.term_definitions[term]
98
+ context = term_context ? context.parse(term_context, propagate: false) : context
59
99
  end
60
100
  end
61
101
 
62
102
  # Process each key and value in element. Ignores @nesting content
63
- expand_object(input, active_property, context, output_object, ordered: ordered, framing: framing)
103
+ expand_object(input, active_property, context, output_object,
104
+ expanded_active_property: expanded_active_property,
105
+ type_scoped_context: type_scoped_context,
106
+ type_key: type_key,
107
+ ordered: ordered,
108
+ framing: framing)
64
109
 
65
110
  #log_debug("output object") {output_object.inspect}
66
111
 
67
112
  # If result contains the key @value:
68
113
  if value?(output_object)
69
- unless (output_object.keys - %w(@value @language @type @index)).empty? &&
70
- !(output_object.key?('@language') && output_object.key?('@type'))
71
- # The result must not contain any keys other than @value, @language, @type, and @index. It must not contain both the @language key and the @type key. Otherwise, an invalid value object error has been detected and processing is aborted.
114
+ keys = output_object.keys
115
+ unless (keys - KEYS_VALUE_LANGUAGE_TYPE_INDEX_DIRECTION).empty?
116
+ # The result must not contain any keys other than @direction, @value, @language, @type, and @index. It must not contain both the @language key and the @type key. Otherwise, an invalid value object error has been detected and processing is aborted.
72
117
  raise JsonLdError::InvalidValueObject,
73
118
  "value object has unknown keys: #{output_object.inspect}"
74
119
  end
75
120
 
121
+ if keys.include?('@type') && !(keys & %w(@language @direction)).empty?
122
+ # @type is inconsistent with either @language or @direction
123
+ raise JsonLdError::InvalidValueObject,
124
+ "value object must not include @type with either @language or @direction: #{output_object.inspect}"
125
+ end
126
+
76
127
  output_object.delete('@language') if output_object.key?('@language') && Array(output_object['@language']).empty?
128
+ type_is_json = output_object['@type'] == '@json'
77
129
  output_object.delete('@type') if output_object.key?('@type') && Array(output_object['@type']).empty?
78
130
 
79
- # If the value of result's @value key is null, then set result to null.
131
+ # If the value of result's @value key is null, then set result to null and @type is not @json.
80
132
  ary = Array(output_object['@value'])
81
- return nil if ary.empty?
133
+ return nil if ary.empty? && !type_is_json
82
134
 
83
- if !ary.all? {|v| v.is_a?(String) || v.is_a?(Hash) && v.empty?} && output_object.has_key?('@language')
135
+ if output_object['@type'] == '@json' && context.processingMode('json-ld-1.1')
136
+ # Any value of @value is okay if @type: @json
137
+ elsif !ary.all? {|v| v.is_a?(String) || v.is_a?(Hash) && v.empty?} && output_object.has_key?('@language')
84
138
  # Otherwise, if the value of result's @value member is not a string and result contains the key @language, an invalid language-tagged value error has been detected (only strings can be language-tagged) and processing is aborted.
85
139
  raise JsonLdError::InvalidLanguageTaggedValue,
86
140
  "when @language is used, @value must be a string: #{output_object.inspect}"
@@ -89,7 +143,7 @@ module JSON::LD
89
143
  t.is_a?(Hash) && t.empty?}
90
144
  # Otherwise, if the result has a @type member and its value is not an IRI, an invalid typed value error has been detected and processing is aborted.
91
145
  raise JsonLdError::InvalidTypedValue,
92
- "value of @type must be an IRI: #{output_object.inspect}"
146
+ "value of @type must be an IRI or '@json': #{output_object.inspect}"
93
147
  end
94
148
  elsif !output_object.fetch('@type', []).is_a?(Array)
95
149
  # Otherwise, if result contains the key @type and its associated value is not an array, set it to an array containing only the associated value.
@@ -99,7 +153,7 @@ module JSON::LD
99
153
  # The result must contain at most one other key and that key must be @index. Otherwise, an invalid set or list object error has been detected and processing is aborted.
100
154
  raise JsonLdError::InvalidSetOrListObject,
101
155
  "@set or @list may only contain @index: #{output_object.keys.inspect}" unless
102
- (output_object.keys - %w(@set @list @index)).empty?
156
+ (output_object.keys - KEYS_SET_LIST_INDEX).empty?
103
157
 
104
158
  # If result contains the key @set, then set result to the key's associated value.
105
159
  return output_object['@set'] if output_object.key?('@set')
@@ -109,9 +163,9 @@ module JSON::LD
109
163
  return nil if output_object.length == 1 && output_object.key?('@language')
110
164
 
111
165
  # If active property is null or @graph, drop free-floating values as follows:
112
- if (active_property || '@graph') == '@graph' &&
166
+ if (expanded_active_property || '@graph') == '@graph' &&
113
167
  (output_object.key?('@value') || output_object.key?('@list') ||
114
- (output_object.keys - %w(@id)).empty? && !framing)
168
+ (output_object.keys - CONTAINER_ID).empty? && !framing)
115
169
  #log_debug(" =>") { "empty top-level: " + output_object.inspect}
116
170
  return nil
117
171
  end
@@ -124,7 +178,11 @@ module JSON::LD
124
178
  end
125
179
  else
126
180
  # Otherwise, unless the value is a number, expand the value according to the Value Expansion rules, passing active property.
127
- return nil if input.nil? || active_property.nil? || active_property == '@graph'
181
+ return nil if input.nil? || active_property.nil? || expanded_active_property == '@graph'
182
+
183
+ # Apply property-scoped context
184
+ context = property_scoped_context ? context.parse(property_scoped_context, override_protected: true) : context
185
+
128
186
  context.expand_value(active_property, input, log_depth: @options[:log_depth])
129
187
  end
130
188
 
@@ -133,12 +191,19 @@ module JSON::LD
133
191
  end
134
192
 
135
193
  private
136
- CONTAINER_MAPPING_INDEX_ID_TYPE = Set.new(%w(@index @id @type)).freeze
137
194
 
138
195
  # Expand each key and value of element adding them to result
139
- def expand_object(input, active_property, context, output_object, ordered:, framing:)
196
+ def expand_object(input, active_property, context, output_object,
197
+ expanded_active_property:,
198
+ type_scoped_context:,
199
+ type_key:,
200
+ ordered:,
201
+ framing:)
140
202
  nests = []
141
203
 
204
+ input_type = Array(input[type_key]).last
205
+ input_type = context.expand_iri(input_type, vocab: true, quiet: true) if input_type
206
+
142
207
  # Then, proceed and process each property and value in element as follows:
143
208
  keys = ordered ? input.keys.sort : input.keys
144
209
  keys.each do |key|
@@ -150,6 +215,11 @@ module JSON::LD
150
215
  next if expanded_property.is_a?(RDF::URI) && expanded_property.relative?
151
216
  expanded_property = expanded_property.to_s if expanded_property.is_a?(RDF::Resource)
152
217
 
218
+ warn "[DEPRECATION] Blank Node properties deprecated in JSON-LD 1.1." if
219
+ @options[:validate] &&
220
+ expanded_property.to_s.start_with?("_:") &&
221
+ context.processingMode('json-ld-1.1')
222
+
153
223
  #log_debug("expand property") {"ap: #{active_property.inspect}, expanded: #{expanded_property.inspect}, value: #{value.inspect}"}
154
224
 
155
225
  if expanded_property.nil?
@@ -160,11 +230,11 @@ module JSON::LD
160
230
  if KEYWORDS.include?(expanded_property)
161
231
  # If active property equals @reverse, an invalid reverse property map error has been detected and processing is aborted.
162
232
  raise JsonLdError::InvalidReversePropertyMap,
163
- "@reverse not appropriate at this point" if active_property == '@reverse'
233
+ "@reverse not appropriate at this point" if expanded_active_property == '@reverse'
164
234
 
165
- # If result has already an expanded property member, an colliding keywords error has been detected and processing is aborted.
235
+ # If result has already an expanded property member (other than @type), an colliding keywords error has been detected and processing is aborted.
166
236
  raise JsonLdError::CollidingKeywords,
167
- "#{expanded_property} already exists in result" if output_object.has_key?(expanded_property)
237
+ "#{expanded_property} already exists in result" if output_object.has_key?(expanded_property) && !KEYS_INCLUDED_TYPE.include?(expanded_property)
168
238
 
169
239
  expanded_value = case expanded_property
170
240
  when '@id'
@@ -200,62 +270,106 @@ module JSON::LD
200
270
  else
201
271
  e_id
202
272
  end
273
+ when '@included'
274
+ # Included blocks are treated as an array of separate object nodes sharing the same referencing active_property. For 1.0, it is skipped as are other unknown keywords
275
+ next if context.processingMode('json-ld-1.0')
276
+ included_result = as_array(expand(value, active_property, context, ordered: ordered, framing: framing))
277
+
278
+ # Expanded values must be node objects
279
+ raise JsonLdError::InvalidIncludedValue, "values of @included must expand to node objects" unless included_result.all? {|e| node?(e)}
280
+ # As other properties may alias to @included, add this to any other previously expanded values
281
+ Array(output_object['@included']) + included_result
203
282
  when '@type'
204
283
  # If expanded property is @type and value is neither a string nor an array of strings, an invalid type value error has been detected and processing is aborted. Otherwise, set expanded value to the result of using the IRI Expansion algorithm, passing active context, true for vocab, and true for document relative to expand the value or each of its items.
205
284
  #log_debug("@type") {"value: #{value.inspect}"}
206
- case value
285
+ e_type = case value
207
286
  when Array
208
287
  value.map do |v|
209
288
  raise JsonLdError::InvalidTypeValue,
210
289
  "@type value must be a string or array of strings: #{v.inspect}" unless v.is_a?(String)
211
- context.expand_iri(v, vocab: true, documentRelative: true, quiet: true).to_s
290
+ type_scoped_context.expand_iri(v, vocab: true, documentRelative: true, quiet: true).to_s
212
291
  end
213
292
  when String
214
- context.expand_iri(value, vocab: true, documentRelative: true, quiet: true).to_s
293
+ type_scoped_context.expand_iri(value, vocab: true, documentRelative: true, quiet: true).to_s
215
294
  when Hash
216
- # For framing
217
- raise JsonLdError::InvalidTypeValue,
218
- "@type value must be a an empty object for framing: #{value.inspect}" unless
219
- value.empty? && framing
220
- [{}]
295
+ if !framing
296
+ raise JsonLdError::InvalidTypeValue,
297
+ "@type value must be a string or array of strings: #{value.inspect}"
298
+ elsif value.keys.length == 1 &&
299
+ type_scoped_context.expand_iri(value.keys.first, vocab: true, quiet: true).to_s == '@default'
300
+ # Expand values of @default, which must be a string, or array of strings expanding to IRIs
301
+ [{'@default' => Array(value['@default']).map do |v|
302
+ raise JsonLdError::InvalidTypeValue,
303
+ "@type default value must be a string or array of strings: #{v.inspect}" unless v.is_a?(String)
304
+ type_scoped_context.expand_iri(v, vocab: true, documentRelative: true, quiet: true).to_s
305
+ end}]
306
+ elsif !value.empty?
307
+ raise JsonLdError::InvalidTypeValue,
308
+ "@type value must be a an empty object for framing: #{value.inspect}"
309
+ else
310
+ [{}]
311
+ end
221
312
  else
222
313
  raise JsonLdError::InvalidTypeValue,
223
314
  "@type value must be a string or array of strings: #{value.inspect}"
224
315
  end
316
+
317
+ e_type = Array(output_object['@type']) + Array(e_type)
318
+ # Use array form if framing
319
+ framing || e_type.length > 1 ? e_type : e_type.first
225
320
  when '@graph'
226
321
  # If expanded property is @graph, set expanded value to the result of using this algorithm recursively passing active context, @graph for active property, and value for element.
227
322
  value = expand(value, '@graph', context, ordered: ordered, framing: framing)
228
323
  as_array(value)
229
324
  when '@value'
230
- # If expanded property is @value and value is not a scalar or null, an invalid value object value error has been detected and processing is aborted. Otherwise, set expanded value to value. If expanded value is null, set the @value member of result to null and continue with the next key from element. Null values need to be preserved in this case as the meaning of an @type member depends on the existence of an @value member.
325
+ # If expanded property is @value and input contains @type: json, accept any value.
326
+ # If expanded property is @value and value is not a scalar or null, an invalid value object value error has been detected and processing is aborted. (In 1.1, @value can have any JSON value of @type is @json or the property coerces to @json).
327
+ # Otherwise, set expanded value to value. If expanded value is null, set the @value member of result to null and continue with the next key from element. Null values need to be preserved in this case as the meaning of an @type member depends on the existence of an @value member.
231
328
  # If framing, always use array form, unless null
232
- case value
233
- when String, TrueClass, FalseClass, Numeric then (framing ? [value] : value)
234
- when nil
235
- output_object['@value'] = nil
236
- next;
237
- when Array
238
- raise JsonLdError::InvalidValueObjectValue,
239
- "@value value may not be an array unless framing: #{value.inspect}" unless framing
329
+ if input_type == '@json' && context.processingMode('json-ld-1.1')
240
330
  value
241
- when Hash
242
- raise JsonLdError::InvalidValueObjectValue,
243
- "@value value must be a an empty object for framing: #{value.inspect}" unless
244
- value.empty? && framing
245
- [value]
246
331
  else
247
- raise JsonLdError::InvalidValueObjectValue,
248
- "Value of #{expanded_property} must be a scalar or null: #{value.inspect}"
332
+ case value
333
+ when String, TrueClass, FalseClass, Numeric then (framing ? [value] : value)
334
+ when nil
335
+ output_object['@value'] = nil
336
+ next;
337
+ when Array
338
+ raise JsonLdError::InvalidValueObjectValue,
339
+ "@value value may not be an array unless framing: #{value.inspect}" unless framing
340
+ value
341
+ when Hash
342
+ raise JsonLdError::InvalidValueObjectValue,
343
+ "@value value must be a an empty object for framing: #{value.inspect}" unless
344
+ value.empty? && framing
345
+ [value]
346
+ else
347
+ raise JsonLdError::InvalidValueObjectValue,
348
+ "Value of #{expanded_property} must be a scalar or null: #{value.inspect}"
349
+ end
249
350
  end
250
351
  when '@language'
251
352
  # If expanded property is @language and value is not a string, an invalid language-tagged string error has been detected and processing is aborted. Otherwise, set expanded value to lowercased value.
252
353
  # If framing, always use array form, unless null
253
354
  case value
254
- when String then (framing ? [value.downcase] : value.downcase)
355
+ when String
356
+ if value !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
357
+ warn "@language must be valid BCP47: #{value.inspect}"
358
+ end
359
+ if @options[:lowercaseLanguage]
360
+ (framing ? [value.downcase] : value.downcase)
361
+ else
362
+ (framing ? [value] : value)
363
+ end
255
364
  when Array
256
365
  raise JsonLdError::InvalidLanguageTaggedString,
257
366
  "@language value may not be an array unless framing: #{value.inspect}" unless framing
258
- value.map(&:downcase)
367
+ value.each do |v|
368
+ if v !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
369
+ warn "@language must be valid BCP47: #{v.inspect}"
370
+ end
371
+ end
372
+ @options[:lowercaseLanguage] ? value.map(&:downcase) : value
259
373
  when Hash
260
374
  raise JsonLdError::InvalidLanguageTaggedString,
261
375
  "@language value must be a an empty object for framing: #{value.inspect}" unless
@@ -265,16 +379,36 @@ module JSON::LD
265
379
  raise JsonLdError::InvalidLanguageTaggedString,
266
380
  "Value of #{expanded_property} must be a string: #{value.inspect}"
267
381
  end
382
+ when '@direction'
383
+ # If expanded property is @direction and value is not either 'ltr' or 'rtl', an invalid base direction error has been detected and processing is aborted. Otherwise, set expanded value to value.
384
+ # If framing, always use array form, unless null
385
+ case value
386
+ when 'ltr', 'rtl' then (framing ? [value] : value)
387
+ when Array
388
+ raise JsonLdError::InvalidBaseDirection,
389
+ "@direction value may not be an array unless framing: #{value.inspect}" unless framing
390
+ raise JsonLdError::InvalidBaseDirection,
391
+ "@direction must be one of 'ltr', 'rtl', or an array of those if framing #{value.inspect}" unless value.all? {|v| %w(ltr rtl).include?(v) || v.is_a?(Hash) && v.empty?}
392
+ value
393
+ when Hash
394
+ raise JsonLdError::InvalidBaseDirection,
395
+ "@direction value must be a an empty object for framing: #{value.inspect}" unless
396
+ value.empty? && framing
397
+ [value]
398
+ else
399
+ raise JsonLdError::InvalidBaseDirection,
400
+ "Value of #{expanded_property} must be one of 'ltr' or 'rtl': #{value.inspect}"
401
+ end
268
402
  when '@index'
269
403
  # If expanded property is @index and value is not a string, an invalid @index value error has been detected and processing is aborted. Otherwise, set expanded value to value.
270
404
  raise JsonLdError::InvalidIndexValue,
271
405
  "Value of @index is not a string: #{value.inspect}" unless value.is_a?(String)
272
406
  value
273
407
  when '@list'
274
- # If expanded property is @list:
408
+ # If expanded property is @graph:
275
409
 
276
410
  # If active property is null or @graph, continue with the next key from element to remove the free-floating list.
277
- next if (active_property || '@graph') == '@graph'
411
+ next if (expanded_active_property || '@graph') == '@graph'
278
412
 
279
413
  # Otherwise, initialize expanded value to the result of using this algorithm recursively passing active context, active property, and value for element.
280
414
  value = expand(value, active_property, context, ordered: ordered, framing: framing)
@@ -339,15 +473,15 @@ module JSON::LD
339
473
 
340
474
  # Unless expanded value is null, set the expanded property member of result to expanded value.
341
475
  #log_debug("expand #{expanded_property}") { expanded_value.inspect}
342
- output_object[expanded_property] = expanded_value unless expanded_value.nil?
476
+ output_object[expanded_property] = expanded_value unless expanded_value.nil? && expanded_property == '@value' && input_type != '@json'
343
477
  next
344
478
  end
345
479
 
346
- # Use a term-specific context, if defined
347
- term_context = context.term_definitions[key].context if context.term_definitions[key]
348
- active_context = term_context ? context.parse(term_context) : context
349
- container = active_context.container(key)
350
- expanded_value = if container.length == 1 && container.first == '@language' && value.is_a?(Hash)
480
+ container = context.container(key)
481
+ expanded_value = if context.coerce(key) == '@json'
482
+ # In JSON-LD 1.1, values can be native JSON
483
+ {"@value" => value, "@type" => "@json"}
484
+ elsif container.length == 1 && container.first == '@language' && value.is_a?(Hash)
351
485
  # Otherwise, if key's container mapping in active context is @language and value is a JSON object then value is expanded from a language map as follows:
352
486
 
353
487
  # Set multilingual array to an empty array.
@@ -356,7 +490,12 @@ module JSON::LD
356
490
  # For each key-value pair language-language value in value, ordered lexicographically by language
357
491
  keys = ordered ? value.keys.sort : value.keys
358
492
  keys.each do |k|
359
- expanded_k = active_context.expand_iri(k, vocab: true, quiet: true).to_s
493
+ expanded_k = context.expand_iri(k, vocab: true, quiet: true).to_s
494
+
495
+ if k !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/ && expanded_k != '@none'
496
+ warn "@language must be valid BCP47: #{k.inspect}"
497
+ end
498
+
360
499
  [value[k]].flatten.each do |item|
361
500
  # item must be a string, otherwise an invalid language map value error has been detected and processing is aborted.
362
501
  raise JsonLdError::InvalidLanguageMapValue,
@@ -364,47 +503,68 @@ module JSON::LD
364
503
 
365
504
  # Append a JSON object to expanded value that consists of two key-value pairs: (@value-item) and (@language-lowercased language).
366
505
  v = {'@value' => item}
367
- v['@language'] = k.downcase unless expanded_k == '@none'
506
+ v['@language'] = (@options[:lowercaseLanguage] ? k.downcase : k) unless expanded_k == '@none'
507
+ v['@direction'] = context.direction(key) if context.direction(key)
368
508
  ary << v if item
369
509
  end
370
510
  end
371
511
 
372
512
  ary
373
- elsif container.any? { |key| CONTAINER_MAPPING_INDEX_ID_TYPE.include?(key) } && value.is_a?(Hash)
513
+ elsif container.any? { |key| CONTAINER_INDEX_ID_TYPE.include?(key) } && value.is_a?(Hash)
374
514
  # Otherwise, if key's container mapping in active context contains @index, @id, @type and value is a JSON object then value is expanded from an index map as follows:
375
515
 
376
516
  # Set ary to an empty array.
377
517
  ary = []
518
+ index_key = context.term_definitions[key].index || '@index'
519
+
520
+ # While processing index keys, if container includes @type, clear type-scoped term definitions
521
+ container_context = if container.include?('@type') && context.previous_context
522
+ context.previous_context
523
+ elsif container.include?('@id') && context.term_definitions[key]
524
+ id_context = context.term_definitions[key].context if context.term_definitions[key]
525
+ id_context ? context.parse(id_context, propagate: false) : context
526
+ else
527
+ context
528
+ end
378
529
 
379
530
  # For each key-value in the object:
380
531
  keys = ordered ? value.keys.sort : value.keys
381
532
  keys.each do |k|
382
533
  # If container mapping in the active context includes @type, and k is a term in the active context having a local context, use that context when expanding values
383
- map_context = active_context.term_definitions[k].context if container.include?('@type') && active_context.term_definitions[k]
384
- map_context = active_context.parse(map_context) if map_context
385
- map_context ||= active_context
534
+ map_context = container_context.term_definitions[k].context if container.include?('@type') && container_context.term_definitions[k]
535
+ map_context = container_context.parse(map_context, propagate: false) if map_context
536
+ map_context ||= container_context
386
537
 
387
- expanded_k = active_context.expand_iri(k, vocab: true, quiet: true).to_s
538
+ expanded_k = container_context.expand_iri(k, vocab: true, quiet: true).to_s
388
539
 
389
540
  # Initialize index value to the result of using this algorithm recursively, passing active context, key as active property, and index value as element.
390
- index_value = expand([value[k]].flatten, key, map_context, ordered: ordered, framing: framing)
541
+ index_value = expand([value[k]].flatten, key, map_context, ordered: ordered, framing: framing, from_map: true)
391
542
  index_value.each do |item|
392
543
  case container
393
- when %w(@graph @index), %w(@index)
544
+ when CONTAINER_GRAPH_INDEX, CONTAINER_INDEX
394
545
  # Indexed graph by graph name
395
546
  if !graph?(item) && container.include?('@graph')
396
547
  item = {'@graph' => as_array(item)}
397
548
  end
398
- item['@index'] ||= k unless expanded_k == '@none'
399
- when %w(@graph @id), %w(@id)
549
+ if index_key == '@index'
550
+ item['@index'] ||= k unless expanded_k == '@none'
551
+ elsif value?(item)
552
+ raise JsonLdError::InvalidValueObject, "Attempt to add illegal key to value object: #{index_key}"
553
+ else
554
+ # Expand key based on term
555
+ expanded_k = k == '@none' ? '@none' : container_context.expand_value(index_key, k)
556
+ index_property = container_context.expand_iri(index_key, vocab: true, quiet: true).to_s
557
+ item[index_property] = [expanded_k].concat(Array(item[index_property])) unless expanded_k == '@none'
558
+ end
559
+ when CONTAINER_GRAPH_ID, CONTAINER_ID
400
560
  # Indexed graph by graph name
401
561
  if !graph?(item) && container.include?('@graph')
402
562
  item = {'@graph' => as_array(item)}
403
563
  end
404
564
  # Expand k document relative
405
- expanded_k = active_context.expand_iri(k, documentRelative: true, quiet: true).to_s unless expanded_k == '@none'
565
+ expanded_k = container_context.expand_iri(k, documentRelative: true, quiet: true).to_s unless expanded_k == '@none'
406
566
  item['@id'] ||= expanded_k unless expanded_k == '@none'
407
- when %w(@type)
567
+ when CONTAINER_TYPE
408
568
  item['@type'] = [expanded_k].concat(Array(item['@type'])) unless expanded_k == '@none'
409
569
  end
410
570
 
@@ -415,7 +575,7 @@ module JSON::LD
415
575
  ary
416
576
  else
417
577
  # Otherwise, initialize expanded value to the result of using this algorithm recursively, passing active context, key for active property, and value for element.
418
- expand(value, key, active_context, ordered: ordered, framing: framing)
578
+ expand(value, key, context, ordered: ordered, framing: framing)
419
579
  end
420
580
 
421
581
  # If expanded value is null, ignore key by continuing to the next key from element.
@@ -436,7 +596,7 @@ module JSON::LD
436
596
  if container.first == '@graph' && container.length == 1
437
597
  #log_debug(" => ") { "convert #{expanded_value.inspect} to list"}
438
598
  expanded_value = as_array(expanded_value).map do |v|
439
- graph?(v) ? v : {'@graph' => as_array(v)}
599
+ {'@graph' => as_array(v)}
440
600
  end
441
601
  end
442
602
 
@@ -474,7 +634,12 @@ module JSON::LD
474
634
  nested_values.each do |nv|
475
635
  raise JsonLdError::InvalidNestValue, nv.inspect unless
476
636
  nv.is_a?(Hash) && nv.keys.none? {|k| context.expand_iri(k, vocab: true) == '@value'}
477
- expand_object(nv, active_property, context, output_object, ordered: ordered, framing: framing)
637
+ expand_object(nv, active_property, context, output_object,
638
+ expanded_active_property: expanded_active_property,
639
+ type_scoped_context: type_scoped_context,
640
+ type_key: type_key,
641
+ ordered: ordered,
642
+ framing: framing)
478
643
  end
479
644
  end
480
645
  end