json-ld 3.2.3 → 3.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/json/ld/api.rb +807 -764
  4. data/lib/json/ld/compact.rb +304 -304
  5. data/lib/json/ld/conneg.rb +179 -161
  6. data/lib/json/ld/context.rb +2080 -1945
  7. data/lib/json/ld/expand.rb +745 -666
  8. data/lib/json/ld/extensions.rb +14 -13
  9. data/lib/json/ld/flatten.rb +257 -247
  10. data/lib/json/ld/format.rb +202 -194
  11. data/lib/json/ld/frame.rb +525 -502
  12. data/lib/json/ld/from_rdf.rb +224 -166
  13. data/lib/json/ld/html/nokogiri.rb +123 -121
  14. data/lib/json/ld/html/rexml.rb +151 -147
  15. data/lib/json/ld/reader.rb +107 -100
  16. data/lib/json/ld/resource.rb +224 -205
  17. data/lib/json/ld/streaming_reader.rb +574 -507
  18. data/lib/json/ld/streaming_writer.rb +93 -92
  19. data/lib/json/ld/to_rdf.rb +171 -167
  20. data/lib/json/ld/utils.rb +270 -264
  21. data/lib/json/ld/version.rb +24 -14
  22. data/lib/json/ld/writer.rb +334 -311
  23. data/lib/json/ld.rb +103 -96
  24. metadata +78 -209
  25. data/spec/api_spec.rb +0 -132
  26. data/spec/compact_spec.rb +0 -3482
  27. data/spec/conneg_spec.rb +0 -373
  28. data/spec/context_spec.rb +0 -2036
  29. data/spec/expand_spec.rb +0 -4496
  30. data/spec/flatten_spec.rb +0 -1203
  31. data/spec/format_spec.rb +0 -115
  32. data/spec/frame_spec.rb +0 -2498
  33. data/spec/from_rdf_spec.rb +0 -1005
  34. data/spec/matchers.rb +0 -20
  35. data/spec/rdfstar_spec.rb +0 -25
  36. data/spec/reader_spec.rb +0 -883
  37. data/spec/resource_spec.rb +0 -76
  38. data/spec/spec_helper.rb +0 -281
  39. data/spec/streaming_reader_spec.rb +0 -237
  40. data/spec/streaming_writer_spec.rb +0 -145
  41. data/spec/suite_compact_spec.rb +0 -22
  42. data/spec/suite_expand_spec.rb +0 -36
  43. data/spec/suite_flatten_spec.rb +0 -34
  44. data/spec/suite_frame_spec.rb +0 -29
  45. data/spec/suite_from_rdf_spec.rb +0 -22
  46. data/spec/suite_helper.rb +0 -411
  47. data/spec/suite_html_spec.rb +0 -22
  48. data/spec/suite_http_spec.rb +0 -35
  49. data/spec/suite_remote_doc_spec.rb +0 -22
  50. data/spec/suite_to_rdf_spec.rb +0 -30
  51. data/spec/support/extensions.rb +0 -44
  52. data/spec/test-files/test-1-compacted.jsonld +0 -10
  53. data/spec/test-files/test-1-context.jsonld +0 -7
  54. data/spec/test-files/test-1-expanded.jsonld +0 -5
  55. data/spec/test-files/test-1-input.jsonld +0 -10
  56. data/spec/test-files/test-1-rdf.ttl +0 -8
  57. data/spec/test-files/test-2-compacted.jsonld +0 -20
  58. data/spec/test-files/test-2-context.jsonld +0 -7
  59. data/spec/test-files/test-2-expanded.jsonld +0 -16
  60. data/spec/test-files/test-2-input.jsonld +0 -20
  61. data/spec/test-files/test-2-rdf.ttl +0 -14
  62. data/spec/test-files/test-3-compacted.jsonld +0 -11
  63. data/spec/test-files/test-3-context.jsonld +0 -8
  64. data/spec/test-files/test-3-expanded.jsonld +0 -10
  65. data/spec/test-files/test-3-input.jsonld +0 -11
  66. data/spec/test-files/test-3-rdf.ttl +0 -8
  67. data/spec/test-files/test-4-compacted.jsonld +0 -10
  68. data/spec/test-files/test-4-context.jsonld +0 -7
  69. data/spec/test-files/test-4-expanded.jsonld +0 -6
  70. data/spec/test-files/test-4-input.jsonld +0 -10
  71. data/spec/test-files/test-4-rdf.ttl +0 -5
  72. data/spec/test-files/test-5-compacted.jsonld +0 -13
  73. data/spec/test-files/test-5-context.jsonld +0 -7
  74. data/spec/test-files/test-5-expanded.jsonld +0 -9
  75. data/spec/test-files/test-5-input.jsonld +0 -13
  76. data/spec/test-files/test-5-rdf.ttl +0 -7
  77. data/spec/test-files/test-6-compacted.jsonld +0 -10
  78. data/spec/test-files/test-6-context.jsonld +0 -7
  79. data/spec/test-files/test-6-expanded.jsonld +0 -10
  80. data/spec/test-files/test-6-input.jsonld +0 -10
  81. data/spec/test-files/test-6-rdf.ttl +0 -6
  82. data/spec/test-files/test-7-compacted.jsonld +0 -23
  83. data/spec/test-files/test-7-context.jsonld +0 -4
  84. data/spec/test-files/test-7-expanded.jsonld +0 -20
  85. data/spec/test-files/test-7-input.jsonld +0 -23
  86. data/spec/test-files/test-7-rdf.ttl +0 -14
  87. data/spec/test-files/test-8-compacted.jsonld +0 -34
  88. data/spec/test-files/test-8-context.jsonld +0 -11
  89. data/spec/test-files/test-8-expanded.jsonld +0 -24
  90. data/spec/test-files/test-8-frame.jsonld +0 -18
  91. data/spec/test-files/test-8-framed.jsonld +0 -25
  92. data/spec/test-files/test-8-input.jsonld +0 -30
  93. data/spec/test-files/test-8-rdf.ttl +0 -15
  94. data/spec/test-files/test-9-compacted.jsonld +0 -20
  95. data/spec/test-files/test-9-context.jsonld +0 -13
  96. data/spec/test-files/test-9-expanded.jsonld +0 -14
  97. data/spec/test-files/test-9-input.jsonld +0 -12
  98. data/spec/to_rdf_spec.rb +0 -1551
  99. data/spec/writer_spec.rb +0 -427
@@ -1,761 +1,840 @@
1
- # -*- encoding: utf-8 -*-
2
1
  # frozen_string_literal: true
2
+
3
3
  require 'set'
4
4
 
5
- module JSON::LD
6
- ##
7
- # Expand module, used as part of API
8
- module Expand
9
- include Utils
5
+ module JSON
6
+ module LD
7
+ ##
8
+ # Expand module, used as part of API
9
+ module Expand
10
+ include Utils
11
+
12
+ # The following constant is used to reduce object allocations
13
+ CONTAINER_INDEX_ID_TYPE = Set['@index', '@id', '@type'].freeze
14
+ KEY_ID = %w[@id].freeze
15
+ KEYS_VALUE_LANGUAGE_TYPE_INDEX_DIRECTION = %w[@value @language @type @index @direction @annotation].freeze
16
+ KEYS_SET_LIST_INDEX = %w[@set @list @index].freeze
17
+ KEYS_INCLUDED_TYPE_REVERSE = %w[@included @type @reverse].freeze
18
+
19
+ ##
20
+ # Expand an Array or Object given an active context and performing local context expansion.
21
+ #
22
+ # @param [Array, Hash] input
23
+ # @param [String] active_property
24
+ # @param [Context] context
25
+ # @param [Boolean] framing (false)
26
+ # Special rules for expanding a frame
27
+ # @param [Boolean] from_map
28
+ # Expanding from a map, which could be an `@type` map, so don't clear out context term definitions
29
+ #
30
+ # @return [Array<Hash{String => Object}>]
31
+ def expand(input, active_property, context,
32
+ framing: false, from_map: false, log_depth: nil)
33
+ # log_debug("expand", depth: log_depth.to_i) {"input: #{input.inspect}, active_property: #{active_property.inspect}, context: #{context.inspect}"}
34
+ framing = false if active_property == '@default'
35
+ if active_property
36
+ expanded_active_property = context.expand_iri(active_property, vocab: true, as_string: true,
37
+ base: @options[:base])
38
+ end
10
39
 
11
- # The following constant is used to reduce object allocations
12
- CONTAINER_INDEX_ID_TYPE = Set['@index', '@id', '@type'].freeze
13
- KEY_ID = %w(@id).freeze
14
- KEYS_VALUE_LANGUAGE_TYPE_INDEX_DIRECTION = %w(@value @language @type @index @direction @annotation).freeze
15
- KEYS_SET_LIST_INDEX = %w(@set @list @index).freeze
16
- KEYS_INCLUDED_TYPE_REVERSE = %w(@included @type @reverse).freeze
40
+ # Use a term-specific context, if defined, based on the non-type-scoped context.
41
+ if active_property && context.term_definitions[active_property]
42
+ property_scoped_context = context.term_definitions[active_property].context
43
+ end
44
+ # log_debug("expand", depth: log_depth.to_i) {"property_scoped_context: #{property_scoped_context.inspect}"} unless property_scoped_context.nil?
45
+
46
+ case input
47
+ when Array
48
+ # If element is an array,
49
+ is_list = context.container(active_property).include?('@list')
50
+ input.each_with_object([]) do |v, memo|
51
+ # Initialize expanded item to the result of using this algorithm recursively, passing active context, active property, and item as element.
52
+ v = expand(v, active_property, context,
53
+ framing: framing,
54
+ from_map: from_map,
55
+ log_depth: log_depth.to_i + 1)
17
56
 
18
- ##
19
- # Expand an Array or Object given an active context and performing local context expansion.
20
- #
21
- # @param [Array, Hash] input
22
- # @param [String] active_property
23
- # @param [Context] context
24
- # @param [Boolean] framing (false)
25
- # Special rules for expanding a frame
26
- # @param [Boolean] from_map
27
- # Expanding from a map, which could be an `@type` map, so don't clear out context term definitions
28
- #
29
- # @return [Array<Hash{String => Object}>]
30
- def expand(input, active_property, context,
31
- framing: false, from_map: false, log_depth: nil)
32
- log_debug("expand", depth: log_depth.to_i) {"input: #{input.inspect}, active_property: #{active_property.inspect}, context: #{context.inspect}"}
33
- framing = false if active_property == '@default'
34
- expanded_active_property = context.expand_iri(active_property, vocab: true, as_string: true, base: @options[:base]) if active_property
35
-
36
- # Use a term-specific context, if defined, based on the non-type-scoped context.
37
- property_scoped_context = context.term_definitions[active_property].context if active_property && context.term_definitions[active_property]
38
- log_debug("expand", depth: log_depth.to_i) {"property_scoped_context: #{property_scoped_context.inspect}"} unless property_scoped_context.nil?
39
-
40
- result = case input
41
- when Array
42
- # If element is an array,
43
- is_list = context.container(active_property).include?('@list')
44
- value = input.each_with_object([]) do |v, memo|
45
- # Initialize expanded item to the result of using this algorithm recursively, passing active context, active property, and item as element.
46
- v = expand(v, active_property, context,
47
- framing: framing,
48
- from_map: from_map,
49
- log_depth: log_depth.to_i + 1)
57
+ # 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
58
+ if is_list && v.is_a?(Array)
59
+ # Make sure that no member of v contains an annotation object
60
+ if v.any? { |n| n.is_a?(Hash) && n.key?('@annotation') }
61
+ raise JsonLdError::InvalidAnnotation,
62
+ "A list element must not contain @annotation."
63
+ end
64
+ v = { "@list" => v }
65
+ end
50
66
 
51
- # 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
52
- if is_list && v.is_a?(Array)
53
- # Make sure that no member of v contains an annotation object
54
- raise JsonLdError::InvalidAnnotation,
55
- "A list element must not contain @annotation." if
56
- v.any? {|n| n.is_a?(Hash) && n.key?('@annotation')}
57
- v = {"@list" => v}
67
+ case v
68
+ when nil then nil
69
+ when Array then memo.concat(v)
70
+ else memo << v
71
+ end
58
72
  end
59
73
 
60
- case v
61
- when nil then nil
62
- when Array then memo.concat(v)
63
- else memo << v
74
+ when Hash
75
+ if context.previous_context
76
+ expanded_key_map = input.keys.inject({}) do |memo, key|
77
+ memo.merge(key => context.expand_iri(key, vocab: true, as_string: true, base: @options[:base]))
78
+ end
79
+ # Revert any previously type-scoped term definitions, unless this is from a map, a value object or a subject reference
80
+ revert_context = !from_map &&
81
+ !expanded_key_map.value?('@value') &&
82
+ expanded_key_map.values != ['@id']
83
+
84
+ # If there's a previous context, the context was type-scoped
85
+ # log_debug("expand", depth: log_depth.to_i) {"previous_context: #{context.previous_context.inspect}"} if revert_context
86
+ context = context.previous_context if revert_context
64
87
  end
65
- end
66
88
 
67
- value
68
- when Hash
69
- if context.previous_context
70
- expanded_key_map = input.keys.inject({}) do |memo, key|
71
- memo.merge(key => context.expand_iri(key, vocab: true, as_string: true, base: @options[:base]))
89
+ # Apply property-scoped context after reverting term-scoped context
90
+ unless property_scoped_context.nil?
91
+ context = context.parse(property_scoped_context, base: @options[:base], override_protected: true)
72
92
  end
73
- # Revert any previously type-scoped term definitions, unless this is from a map, a value object or a subject reference
74
- revert_context = !from_map &&
75
- !expanded_key_map.values.include?('@value') &&
76
- !(expanded_key_map.values == ['@id'])
77
-
78
- # If there's a previous context, the context was type-scoped
79
- log_debug("expand", depth: log_depth.to_i) {"previous_context: #{context.previous_context.inspect}"} if revert_context
80
- context = context.previous_context if revert_context
81
- end
93
+ # log_debug("expand", depth: log_depth.to_i) {"after property_scoped_context: #{context.inspect}"} unless property_scoped_context.nil?
82
94
 
83
- # Apply property-scoped context after reverting term-scoped context
84
- unless property_scoped_context.nil?
85
- context = context.parse(property_scoped_context, base: @options[:base], override_protected: true)
86
- end
87
- log_debug("expand", depth: log_depth.to_i) {"after property_scoped_context: #{context.inspect}"} unless property_scoped_context.nil?
88
-
89
- # 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.
90
- if input.key?('@context')
91
- context = context.parse(input['@context'], base: @options[:base])
92
- log_debug("expand", depth: log_depth.to_i) {"context: #{context.inspect}"}
93
- end
94
-
95
- # Set the type-scoped context to the context on input, for use later
96
- type_scoped_context = context
95
+ # 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.
96
+ if input.key?('@context')
97
+ context = context.parse(input['@context'], base: @options[:base])
98
+ # log_debug("expand", depth: log_depth.to_i) {"context: #{context.inspect}"}
99
+ end
97
100
 
98
- output_object = {}
101
+ # Set the type-scoped context to the context on input, for use later
102
+ type_scoped_context = context
99
103
 
100
- # See if keys mapping to @type have terms with a local context
101
- type_key = nil
102
- (input.keys - %w(@context)).sort.
103
- select {|k| context.expand_iri(k, vocab: true, base: @options[:base]) == '@type'}.
104
- each do |tk|
104
+ output_object = {}
105
105
 
106
- type_key ||= tk # Side effect saves the first found key mapping to @type
107
- Array(input[tk]).sort.each do |term|
108
- term_context = type_scoped_context.term_definitions[term].context if type_scoped_context.term_definitions[term]
109
- unless term_context.nil?
110
- log_debug("expand", depth: log_depth.to_i) {"term_context[#{term}]: #{term_context.inspect}"}
111
- context = context.parse(term_context, base: @options[:base], propagate: false)
106
+ # See if keys mapping to @type have terms with a local context
107
+ type_key = nil
108
+ (input.keys - %w[@context]).sort
109
+ .select { |k| context.expand_iri(k, vocab: true, base: @options[:base]) == '@type' }
110
+ .each do |tk|
111
+ type_key ||= tk # Side effect saves the first found key mapping to @type
112
+ Array(input[tk]).sort.each do |term|
113
+ if type_scoped_context.term_definitions[term]
114
+ term_context = type_scoped_context.term_definitions[term].context
115
+ end
116
+ unless term_context.nil?
117
+ # log_debug("expand", depth: log_depth.to_i) {"term_context[#{term}]: #{term_context.inspect}"}
118
+ context = context.parse(term_context, base: @options[:base], propagate: false)
119
+ end
112
120
  end
113
121
  end
114
- end
115
122
 
116
- # Process each key and value in element. Ignores @nesting content
117
- expand_object(input, active_property, context, output_object,
118
- expanded_active_property: expanded_active_property,
119
- framing: framing,
120
- type_key: type_key,
121
- type_scoped_context: type_scoped_context,
122
- log_depth: log_depth.to_i + 1)
123
-
124
- log_debug("output object", depth: log_depth.to_i) {output_object.inspect}
125
-
126
- # If result contains the key @value:
127
- if value?(output_object)
128
- keys = output_object.keys
129
- unless (keys - KEYS_VALUE_LANGUAGE_TYPE_INDEX_DIRECTION).empty?
130
- # 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.
131
- raise JsonLdError::InvalidValueObject,
132
- "value object has unknown keys: #{output_object.inspect}"
133
- end
123
+ # Process each key and value in element. Ignores @nesting content
124
+ expand_object(input, active_property, context, output_object,
125
+ expanded_active_property: expanded_active_property,
126
+ framing: framing,
127
+ type_key: type_key,
128
+ type_scoped_context: type_scoped_context,
129
+ log_depth: log_depth.to_i + 1)
134
130
 
135
- if keys.include?('@type') && !(keys & %w(@language @direction)).empty?
136
- # @type is inconsistent with either @language or @direction
137
- raise JsonLdError::InvalidValueObject,
138
- "value object must not include @type with either @language or @direction: #{output_object.inspect}"
139
- end
131
+ # log_debug("output object", depth: log_depth.to_i) {output_object.inspect}
132
+
133
+ # If result contains the key @value:
134
+ if value?(output_object)
135
+ keys = output_object.keys
136
+ unless (keys - KEYS_VALUE_LANGUAGE_TYPE_INDEX_DIRECTION).empty?
137
+ # 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.
138
+ raise JsonLdError::InvalidValueObject,
139
+ "value object has unknown keys: #{output_object.inspect}"
140
+ end
140
141
 
141
- output_object.delete('@language') if output_object.key?('@language') && Array(output_object['@language']).empty?
142
- type_is_json = output_object['@type'] == '@json'
143
- output_object.delete('@type') if output_object.key?('@type') && Array(output_object['@type']).empty?
144
-
145
- # If the value of result's @value key is null, then set result to null and @type is not @json.
146
- ary = Array(output_object['@value'])
147
- return nil if ary.empty? && !type_is_json
148
-
149
- if output_object['@type'] == '@json' && context.processingMode('json-ld-1.1')
150
- # Any value of @value is okay if @type: @json
151
- elsif !ary.all? {|v| v.is_a?(String) || v.is_a?(Hash) && v.empty?} && output_object.key?('@language')
152
- # 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.
153
- raise JsonLdError::InvalidLanguageTaggedValue,
154
- "when @language is used, @value must be a string: #{output_object.inspect}"
155
- elsif output_object['@type'] &&
156
- (!Array(output_object['@type']).all? {|t|
157
- t.is_a?(String) && RDF::URI(t).valid? && !t.start_with?('_:') ||
158
- t.is_a?(Hash) && t.empty?} ||
159
- !framing && !output_object['@type'].is_a?(String))
160
- # 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.
161
- raise JsonLdError::InvalidTypedValue,
162
- "value of @type must be an IRI or '@json': #{output_object.inspect}"
163
- elsif !framing && !output_object.fetch('@type', '').is_a?(String) &&
142
+ if keys.include?('@type') && !(keys & %w[@language @direction]).empty?
143
+ # @type is inconsistent with either @language or @direction
144
+ raise JsonLdError::InvalidValueObject,
145
+ "value object must not include @type with either @language or @direction: #{output_object.inspect}"
146
+ end
147
+
148
+ if output_object.key?('@language') && Array(output_object['@language']).empty?
149
+ output_object.delete('@language')
150
+ end
151
+ type_is_json = output_object['@type'] == '@json'
152
+ output_object.delete('@type') if output_object.key?('@type') && Array(output_object['@type']).empty?
153
+
154
+ # If the value of result's @value key is null, then set result to null and @type is not @json.
155
+ ary = Array(output_object['@value'])
156
+ return nil if ary.empty? && !type_is_json
157
+
158
+ if output_object['@type'] == '@json' && context.processingMode('json-ld-1.1')
159
+ # Any value of @value is okay if @type: @json
160
+ elsif !ary.all? { |v| v.is_a?(String) || (v.is_a?(Hash) && v.empty?) } && output_object.key?('@language')
161
+ # 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.
162
+ raise JsonLdError::InvalidLanguageTaggedValue,
163
+ "when @language is used, @value must be a string: #{output_object.inspect}"
164
+ elsif output_object['@type'] &&
165
+ (!Array(output_object['@type']).all? do |t|
166
+ (t.is_a?(String) && RDF::URI(t).valid? && !t.start_with?('_:')) ||
167
+ (t.is_a?(Hash) && t.empty?)
168
+ end ||
169
+ (!framing && !output_object['@type'].is_a?(String)))
170
+ # 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.
171
+ raise JsonLdError::InvalidTypedValue,
172
+ "value of @type must be an IRI or '@json': #{output_object.inspect}"
173
+ elsif !framing && !output_object.fetch('@type', '').is_a?(String) &&
164
174
  RDF::URI(t).valid? && !t.start_with?('_:')
165
- # 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.
166
- raise JsonLdError::InvalidTypedValue,
167
- "value of @type must be an IRI or '@json': #{output_object.inspect}"
175
+ # 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.
176
+ raise JsonLdError::InvalidTypedValue,
177
+ "value of @type must be an IRI or '@json': #{output_object.inspect}"
178
+ end
179
+ elsif !output_object.fetch('@type', []).is_a?(Array)
180
+ # 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.
181
+ output_object['@type'] = [output_object['@type']]
182
+ elsif output_object.key?('@set') || output_object.key?('@list')
183
+ # Otherwise, if result contains the key @set or @list:
184
+ # 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.
185
+ unless (output_object.keys - KEYS_SET_LIST_INDEX).empty?
186
+ raise JsonLdError::InvalidSetOrListObject,
187
+ "@set or @list may only contain @index: #{output_object.keys.inspect}"
188
+ end
189
+
190
+ # If result contains the key @set, then set result to the key's associated value.
191
+ return output_object['@set'] if output_object.key?('@set')
192
+ elsif output_object['@annotation']
193
+ # Otherwise, if result contains the key @annotation,
194
+ # the array value must all be node objects without an @id property, otherwise, an invalid annotation error has been detected and processing is aborted.
195
+ unless output_object['@annotation'].all? { |o| node?(o) && !o.key?('@id') }
196
+ raise JsonLdError::InvalidAnnotation,
197
+ "@annotation must reference node objects without @id."
198
+ end
199
+
200
+ # Additionally, the property must not be used if there is no active property, or the expanded active property is @graph.
201
+ if %w[@graph @included].include?(expanded_active_property || '@graph')
202
+ raise JsonLdError::InvalidAnnotation,
203
+ "@annotation must not be used on a top-level object."
204
+ end
205
+
168
206
  end
169
- elsif !output_object.fetch('@type', []).is_a?(Array)
170
- # 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.
171
- output_object['@type'] = [output_object['@type']]
172
- elsif output_object.key?('@set') || output_object.key?('@list')
173
- # Otherwise, if result contains the key @set or @list:
174
- # 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.
175
- raise JsonLdError::InvalidSetOrListObject,
176
- "@set or @list may only contain @index: #{output_object.keys.inspect}" unless
177
- (output_object.keys - KEYS_SET_LIST_INDEX).empty?
178
-
179
- # If result contains the key @set, then set result to the key's associated value.
180
- return output_object['@set'] if output_object.key?('@set')
181
- elsif output_object['@annotation']
182
- # Otherwise, if result contains the key @annotation,
183
- # the array value must all be node objects without an @id property, otherwise, an invalid annotation error has been detected and processing is aborted.
184
- raise JsonLdError::InvalidAnnotation,
185
- "@annotation must reference node objects without @id." unless
186
- output_object['@annotation'].all? {|o| node?(o) && !o.key?('@id')}
187
-
188
- # Additionally, the property must not be used if there is no active property, or the expanded active property is @graph.
189
- raise JsonLdError::InvalidAnnotation,
190
- "@annotation must not be used on a top-level object." if
191
- %w(@graph @included).include?(expanded_active_property || '@graph')
192
-
193
- end
194
207
 
195
- # If result contains only the key @language, set result to null.
196
- return nil if output_object.length == 1 && output_object.key?('@language')
208
+ # If result contains only the key @language, set result to null.
209
+ return nil if output_object.length == 1 && output_object.key?('@language')
197
210
 
198
- # If active property is null or @graph, drop free-floating values as follows:
199
- if (expanded_active_property || '@graph') == '@graph' &&
200
- (output_object.key?('@value') || output_object.key?('@list') ||
201
- (output_object.keys - KEY_ID).empty? && !framing)
202
- log_debug(" =>", depth: log_depth.to_i) { "empty top-level: " + output_object.inspect}
203
- return nil
204
- end
211
+ # If active property is null or @graph, drop free-floating values as follows:
212
+ if (expanded_active_property || '@graph') == '@graph' &&
213
+ (output_object.key?('@value') || output_object.key?('@list') ||
214
+ ((output_object.keys - KEY_ID).empty? && !framing))
215
+ # log_debug(" =>", depth: log_depth.to_i) { "empty top-level: " + output_object.inspect}
216
+ return nil
217
+ end
205
218
 
206
- # Re-order result keys if ordering
207
- if @options[:ordered]
208
- output_object.keys.sort.each_with_object({}) {|kk, memo| memo[kk] = output_object[kk]}
219
+ # Re-order result keys if ordering
220
+ if @options[:ordered]
221
+ output_object.keys.sort.each_with_object({}) { |kk, memo| memo[kk] = output_object[kk] }
222
+ else
223
+ output_object
224
+ end
209
225
  else
210
- output_object
211
- end
212
- else
213
- # Otherwise, unless the value is a number, expand the value according to the Value Expansion rules, passing active property.
214
- return nil if input.nil? || active_property.nil? || expanded_active_property == '@graph'
215
-
216
- # Apply property-scoped context
217
- unless property_scoped_context.nil?
218
- context = context.parse(property_scoped_context,
219
- base: @options[:base],
220
- override_protected: true)
226
+ # Otherwise, unless the value is a number, expand the value according to the Value Expansion rules, passing active property.
227
+ return nil if input.nil? || active_property.nil? || expanded_active_property == '@graph'
228
+
229
+ # Apply property-scoped context
230
+ unless property_scoped_context.nil?
231
+ context = context.parse(property_scoped_context,
232
+ base: @options[:base],
233
+ override_protected: true)
234
+ end
235
+ # log_debug("expand", depth: log_depth.to_i) {"property_scoped_context: #{context.inspect}"} unless property_scoped_context.nil?
236
+
237
+ context.expand_value(active_property, input, base: @options[:base])
221
238
  end
222
- log_debug("expand", depth: log_depth.to_i) {"property_scoped_context: #{context.inspect}"} unless property_scoped_context.nil?
223
239
 
224
- context.expand_value(active_property, input, base: @options[:base])
240
+ # log_debug(depth: log_depth.to_i) {" => #{result.inspect}"}
225
241
  end
226
242
 
227
- log_debug(depth: log_depth.to_i) {" => #{result.inspect}"}
228
- result
229
- end
243
+ private
230
244
 
231
- private
232
-
233
- # Expand each key and value of element adding them to result
234
- def expand_object(input, active_property, context, output_object,
235
- expanded_active_property:,
236
- framing:,
237
- type_key:,
238
- type_scoped_context:,
239
- log_depth: nil)
240
- nests = []
241
-
242
- input_type = Array(input[type_key]).last
243
- input_type = context.expand_iri(input_type, vocab: true, as_string: true, base: @options[:base]) if input_type
244
-
245
- # Then, proceed and process each property and value in element as follows:
246
- keys = @options[:ordered] ? input.keys.sort : input.keys
247
- keys.each do |key|
248
- # For each key and value in element, ordered lexicographically by key:
249
- value = input[key]
250
- expanded_property = context.expand_iri(key, vocab: true, base: @options[:base])
251
-
252
- # If expanded property is null or it neither contains a colon (:) nor it is a keyword, drop key by continuing to the next key.
253
- next if expanded_property.is_a?(RDF::URI) && expanded_property.relative?
254
- expanded_property = expanded_property.to_s if expanded_property.is_a?(RDF::Resource)
255
-
256
- warn "[DEPRECATION] Blank Node properties deprecated in JSON-LD 1.1." if
257
- @options[:validate] &&
258
- expanded_property.to_s.start_with?("_:") &&
259
- context.processingMode('json-ld-1.1')
260
-
261
- log_debug("expand property", depth: log_depth.to_i) {"ap: #{active_property.inspect}, expanded: #{expanded_property.inspect}, value: #{value.inspect}"}
262
-
263
- if expanded_property.nil?
264
- log_debug(" => ", depth: log_depth.to_i) {"skip nil property"}
265
- next
266
- end
245
+ # Expand each key and value of element adding them to result
246
+ def expand_object(input, active_property, context, output_object,
247
+ expanded_active_property:,
248
+ framing:,
249
+ type_key:,
250
+ type_scoped_context:,
251
+ log_depth: nil)
252
+ nests = []
267
253
 
268
- if KEYWORDS.include?(expanded_property)
269
- # If active property equals @reverse, an invalid reverse property map error has been detected and processing is aborted.
270
- raise JsonLdError::InvalidReversePropertyMap,
271
- "@reverse not appropriate at this point" if expanded_active_property == '@reverse'
272
-
273
- # If result has already an expanded property member (other than @type), an colliding keywords error has been detected and processing is aborted.
274
- raise JsonLdError::CollidingKeywords,
275
- "#{expanded_property} already exists in result" if output_object.key?(expanded_property) && !KEYS_INCLUDED_TYPE_REVERSE.include?(expanded_property)
276
-
277
- expanded_value = case expanded_property
278
- when '@id'
279
- # If expanded active property is `@annotation`, an invalid annotation error has been found and processing is aborted.
280
- raise JsonLdError::InvalidAnnotation,
281
- "an annotation must not contain a property expanding to @id" if
282
- expanded_active_property == '@annotation' && @options[:rdfstar]
283
-
284
- # If expanded property is @id and value is not a string, an invalid @id value error has been detected and processing is aborted
285
- e_id = case value
286
- when String
287
- context.expand_iri(value, as_string: true, base: @options[:base], documentRelative: true)
288
- when Array
289
- # Framing allows an array of IRIs, and always puts values in an array
290
- raise JsonLdError::InvalidIdValue,
291
- "value of @id must be a string unless framing: #{value.inspect}" unless framing
292
- context.expand_iri(value, as_string: true, base: @options[:base], documentRelative: true)
293
- value.map do |v|
294
- raise JsonLdError::InvalidTypeValue,
295
- "@id value must be a string or array of strings for framing: #{v.inspect}" unless v.is_a?(String)
296
- context.expand_iri(v, as_string: true, base: @options[:base], documentRelative: true)
254
+ input_type = Array(input[type_key]).last
255
+ input_type = context.expand_iri(input_type, vocab: true, as_string: true, base: @options[:base]) if input_type
256
+
257
+ # Then, proceed and process each property and value in element as follows:
258
+ keys = @options[:ordered] ? input.keys.sort : input.keys
259
+ keys.each do |key|
260
+ # For each key and value in element, ordered lexicographically by key:
261
+ value = input[key]
262
+ expanded_property = context.expand_iri(key, vocab: true, base: @options[:base])
263
+
264
+ # If expanded property is null or it neither contains a colon (:) nor it is a keyword, drop key by continuing to the next key.
265
+ next if expanded_property.is_a?(RDF::URI) && expanded_property.relative?
266
+
267
+ expanded_property = expanded_property.to_s if expanded_property.is_a?(RDF::Resource)
268
+
269
+ warn "[DEPRECATION] Blank Node properties deprecated in JSON-LD 1.1." if
270
+ @options[:validate] &&
271
+ expanded_property.to_s.start_with?("_:") &&
272
+ context.processingMode('json-ld-1.1')
273
+
274
+ # log_debug("expand property", depth: log_depth.to_i) {"ap: #{active_property.inspect}, expanded: #{expanded_property.inspect}, value: #{value.inspect}"}
275
+
276
+ if expanded_property.nil?
277
+ # log_debug(" => ", depth: log_depth.to_i) {"skip nil property"}
278
+ next
279
+ end
280
+
281
+ if KEYWORDS.include?(expanded_property)
282
+ # If active property equals @reverse, an invalid reverse property map error has been detected and processing is aborted.
283
+ if expanded_active_property == '@reverse'
284
+ raise JsonLdError::InvalidReversePropertyMap,
285
+ "@reverse not appropriate at this point"
286
+ end
287
+
288
+ # If result has already an expanded property member (other than @type), an colliding keywords error has been detected and processing is aborted.
289
+ if output_object.key?(expanded_property) && !KEYS_INCLUDED_TYPE_REVERSE.include?(expanded_property)
290
+ raise JsonLdError::CollidingKeywords,
291
+ "#{expanded_property} already exists in result"
292
+ end
293
+
294
+ expanded_value = case expanded_property
295
+ when '@id'
296
+ # If expanded active property is `@annotation`, an invalid annotation error has been found and processing is aborted.
297
+ if expanded_active_property == '@annotation' && @options[:rdfstar]
298
+ raise JsonLdError::InvalidAnnotation,
299
+ "an annotation must not contain a property expanding to @id"
297
300
  end
298
- when Hash
299
- if framing
300
- raise JsonLdError::InvalidTypeValue,
301
- "value of @id must be a an empty object for framing: #{value.inspect}" unless
302
- value.empty?
303
- [{}]
304
- elsif @options[:rdfstar]
305
- # Result must have just a single statement
306
- rei_node = expand(value, nil, context, log_depth: log_depth.to_i + 1)
307
-
308
- # Node must not contain @reverse
309
- raise JsonLdError::InvalidEmbeddedNode,
310
- "Embedded node with @reverse" if rei_node && rei_node.key?('@reverse')
311
- statements = to_enum(:item_to_rdf, rei_node)
312
- raise JsonLdError::InvalidEmbeddedNode,
313
- "Embedded node with #{statements.size} statements" unless
314
- statements.count == 1
315
- rei_node
301
+
302
+ # If expanded property is @id and value is not a string, an invalid @id value error has been detected and processing is aborted
303
+ e_id = case value
304
+ when String
305
+ context.expand_iri(value, as_string: true, base: @options[:base], documentRelative: true)
306
+ when Array
307
+ # Framing allows an array of IRIs, and always puts values in an array
308
+ unless framing
309
+ raise JsonLdError::InvalidIdValue,
310
+ "value of @id must be a string unless framing: #{value.inspect}"
311
+ end
312
+ context.expand_iri(value, as_string: true, base: @options[:base], documentRelative: true)
313
+ value.map do |v|
314
+ unless v.is_a?(String)
315
+ raise JsonLdError::InvalidTypeValue,
316
+ "@id value must be a string or array of strings for framing: #{v.inspect}"
317
+ end
318
+ context.expand_iri(v, as_string: true, base: @options[:base], documentRelative: true)
319
+ end
320
+ when Hash
321
+ if framing
322
+ unless value.empty?
323
+ raise JsonLdError::InvalidTypeValue,
324
+ "value of @id must be a an empty object for framing: #{value.inspect}"
325
+ end
326
+ [{}]
327
+ elsif @options[:rdfstar]
328
+ # Result must have just a single statement
329
+ rei_node = expand(value, nil, context, log_depth: log_depth.to_i + 1)
330
+
331
+ # Node must not contain @reverse
332
+ if rei_node&.key?('@reverse')
333
+ raise JsonLdError::InvalidEmbeddedNode,
334
+ "Embedded node with @reverse"
335
+ end
336
+ statements = to_enum(:item_to_rdf, rei_node)
337
+ unless statements.count == 1
338
+ raise JsonLdError::InvalidEmbeddedNode,
339
+ "Embedded node with #{statements.size} statements"
340
+ end
341
+ rei_node
342
+ else
343
+ unless framing
344
+ raise JsonLdError::InvalidIdValue,
345
+ "value of @id must be a string unless framing: #{value.inspect}"
346
+ end
347
+ end
316
348
  else
317
349
  raise JsonLdError::InvalidIdValue,
318
- "value of @id must be a string unless framing: #{value.inspect}" unless framing
350
+ "value of @id must be a string or hash if framing: #{value.inspect}"
319
351
  end
320
- else
321
- raise JsonLdError::InvalidIdValue,
322
- "value of @id must be a string or hash if framing: #{value.inspect}"
323
- end
324
352
 
325
- # Use array form if framing
326
- if framing
327
- as_array(e_id)
328
- else
329
- e_id
330
- end
331
- when '@included'
332
- # 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
333
- next if context.processingMode('json-ld-1.0')
334
- included_result = as_array(expand(value, active_property, context,
335
- framing: framing,
336
- log_depth: log_depth.to_i + 1))
337
-
338
- # Expanded values must be node objects
339
- raise JsonLdError::InvalidIncludedValue, "values of @included must expand to node objects" unless included_result.all? {|e| node?(e)}
340
- # As other properties may alias to @included, add this to any other previously expanded values
341
- Array(output_object['@included']) + included_result
342
- when '@type'
343
- # 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.
344
- log_debug("@type", depth: log_depth.to_i) {"value: #{value.inspect}"}
345
- e_type = case value
346
- when Array
347
- value.map do |v|
348
- raise JsonLdError::InvalidTypeValue,
349
- "@type value must be a string or array of strings: #{v.inspect}" unless v.is_a?(String)
350
- type_scoped_context.expand_iri(v,
351
- as_string: true,
352
- base: @options[:base],
353
- documentRelative: true,
354
- vocab: true)
353
+ # Use array form if framing
354
+ if framing
355
+ as_array(e_id)
356
+ else
357
+ e_id
355
358
  end
356
- when String
357
- type_scoped_context.expand_iri(value,
358
- as_string: true,
359
- base: @options[:base],
360
- documentRelative: true,
361
- vocab: true)
362
- when Hash
363
- if !framing
364
- raise JsonLdError::InvalidTypeValue,
365
- "@type value must be a string or array of strings: #{value.inspect}"
366
- elsif value.keys.length == 1 &&
367
- type_scoped_context.expand_iri(value.keys.first, vocab: true, base: @options[:base]) == '@default'
368
- # Expand values of @default, which must be a string, or array of strings expanding to IRIs
369
- [{'@default' => Array(value['@default']).map do |v|
370
- raise JsonLdError::InvalidTypeValue,
371
- "@type default value must be a string or array of strings: #{v.inspect}" unless v.is_a?(String)
359
+ when '@included'
360
+ # 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
361
+ next if context.processingMode('json-ld-1.0')
362
+
363
+ included_result = as_array(expand(value, active_property, context,
364
+ framing: framing,
365
+ log_depth: log_depth.to_i + 1))
366
+
367
+ # Expanded values must be node objects
368
+ unless included_result.all? do |e|
369
+ node?(e)
370
+ end
371
+ raise JsonLdError::InvalidIncludedValue,
372
+ "values of @included must expand to node objects"
373
+ end
374
+
375
+ # As other properties may alias to @included, add this to any other previously expanded values
376
+ Array(output_object['@included']) + included_result
377
+ when '@type'
378
+ # 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.
379
+ # log_debug("@type", depth: log_depth.to_i) {"value: #{value.inspect}"}
380
+ e_type = case value
381
+ when Array
382
+ value.map do |v|
383
+ unless v.is_a?(String)
384
+ raise JsonLdError::InvalidTypeValue,
385
+ "@type value must be a string or array of strings: #{v.inspect}"
386
+ end
372
387
  type_scoped_context.expand_iri(v,
373
388
  as_string: true,
374
389
  base: @options[:base],
375
390
  documentRelative: true,
376
391
  vocab: true)
377
- end}]
378
- elsif !value.empty?
379
- raise JsonLdError::InvalidTypeValue,
380
- "@type value must be a an empty object for framing: #{value.inspect}"
392
+ end
393
+ when String
394
+ type_scoped_context.expand_iri(value,
395
+ as_string: true,
396
+ base: @options[:base],
397
+ documentRelative: true,
398
+ vocab: true)
399
+ when Hash
400
+ if !framing
401
+ raise JsonLdError::InvalidTypeValue,
402
+ "@type value must be a string or array of strings: #{value.inspect}"
403
+ elsif value.keys.length == 1 &&
404
+ type_scoped_context.expand_iri(value.keys.first, vocab: true, base: @options[:base]) == '@default'
405
+ # Expand values of @default, which must be a string, or array of strings expanding to IRIs
406
+ [{ '@default' => Array(value['@default']).map do |v|
407
+ unless v.is_a?(String)
408
+ raise JsonLdError::InvalidTypeValue,
409
+ "@type default value must be a string or array of strings: #{v.inspect}"
410
+ end
411
+ type_scoped_context.expand_iri(v,
412
+ as_string: true,
413
+ base: @options[:base],
414
+ documentRelative: true,
415
+ vocab: true)
416
+ end }]
417
+ elsif !value.empty?
418
+ raise JsonLdError::InvalidTypeValue,
419
+ "@type value must be a an empty object for framing: #{value.inspect}"
420
+ else
421
+ [{}]
422
+ end
381
423
  else
382
- [{}]
424
+ raise JsonLdError::InvalidTypeValue,
425
+ "@type value must be a string or array of strings: #{value.inspect}"
383
426
  end
384
- else
385
- raise JsonLdError::InvalidTypeValue,
386
- "@type value must be a string or array of strings: #{value.inspect}"
387
- end
388
427
 
389
- e_type = Array(output_object['@type']) + Array(e_type)
390
- # Use array form if framing
391
- framing || e_type.length > 1 ? e_type : e_type.first
392
- when '@graph'
393
- # 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.
394
- value = expand(value, '@graph', context,
395
- framing: framing,
396
- log_depth: log_depth.to_i + 1)
397
- as_array(value)
398
- when '@value'
399
- # If expanded property is @value and input contains @type: json, accept any value.
400
- # 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).
401
- # 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.
402
- # If framing, always use array form, unless null
403
- if input_type == '@json' && context.processingMode('json-ld-1.1')
404
- value
405
- else
428
+ e_type = Array(output_object['@type']) + Array(e_type)
429
+ # Use array form if framing
430
+ framing || e_type.length > 1 ? e_type : e_type.first
431
+ when '@graph'
432
+ # 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.
433
+ value = expand(value, '@graph', context,
434
+ framing: framing,
435
+ log_depth: log_depth.to_i + 1)
436
+ as_array(value)
437
+ when '@value'
438
+ # If expanded property is @value and input contains @type: json, accept any value.
439
+ # 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).
440
+ # 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.
441
+ # If framing, always use array form, unless null
442
+ if input_type == '@json' && context.processingMode('json-ld-1.1')
443
+ value
444
+ else
445
+ case value
446
+ when String, TrueClass, FalseClass, Numeric then (framing ? [value] : value)
447
+ when nil
448
+ output_object['@value'] = nil
449
+ next
450
+ when Array
451
+ unless framing
452
+ raise JsonLdError::InvalidValueObjectValue,
453
+ "@value value may not be an array unless framing: #{value.inspect}"
454
+ end
455
+ value
456
+ when Hash
457
+ unless value.empty? && framing
458
+ raise JsonLdError::InvalidValueObjectValue,
459
+ "@value value must be a an empty object for framing: #{value.inspect}"
460
+ end
461
+ [value]
462
+ else
463
+ raise JsonLdError::InvalidValueObjectValue,
464
+ "Value of #{expanded_property} must be a scalar or null: #{value.inspect}"
465
+ end
466
+ end
467
+ when '@language'
468
+ # 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.
469
+ # If framing, always use array form, unless null
406
470
  case value
407
- when String, TrueClass, FalseClass, Numeric then (framing ? [value] : value)
408
- when nil
409
- output_object['@value'] = nil
410
- next;
471
+ when String
472
+ unless /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/.match?(value)
473
+ warn "@language must be valid BCP47: #{value.inspect}"
474
+ end
475
+ if @options[:lowercaseLanguage]
476
+ (framing ? [value.downcase] : value.downcase)
477
+ else
478
+ (framing ? [value] : value)
479
+ end
411
480
  when Array
412
- raise JsonLdError::InvalidValueObjectValue,
413
- "@value value may not be an array unless framing: #{value.inspect}" unless framing
414
- value
481
+ unless framing
482
+ raise JsonLdError::InvalidLanguageTaggedString,
483
+ "@language value may not be an array unless framing: #{value.inspect}"
484
+ end
485
+ value.each do |v|
486
+ unless /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/.match?(v)
487
+ warn "@language must be valid BCP47: #{v.inspect}"
488
+ end
489
+ end
490
+ @options[:lowercaseLanguage] ? value.map(&:downcase) : value
415
491
  when Hash
416
- raise JsonLdError::InvalidValueObjectValue,
417
- "@value value must be a an empty object for framing: #{value.inspect}" unless
418
- value.empty? && framing
492
+ unless value.empty? && framing
493
+ raise JsonLdError::InvalidLanguageTaggedString,
494
+ "@language value must be a an empty object for framing: #{value.inspect}"
495
+ end
419
496
  [value]
420
497
  else
421
- raise JsonLdError::InvalidValueObjectValue,
422
- "Value of #{expanded_property} must be a scalar or null: #{value.inspect}"
423
- end
424
- end
425
- when '@language'
426
- # 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.
427
- # If framing, always use array form, unless null
428
- case value
429
- when String
430
- if value !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
431
- warn "@language must be valid BCP47: #{value.inspect}"
498
+ raise JsonLdError::InvalidLanguageTaggedString,
499
+ "Value of #{expanded_property} must be a string: #{value.inspect}"
432
500
  end
433
- if @options[:lowercaseLanguage]
434
- (framing ? [value.downcase] : value.downcase)
501
+ when '@direction'
502
+ # 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.
503
+ # If framing, always use array form, unless null
504
+ case value
505
+ when 'ltr', 'rtl' then (framing ? [value] : value)
506
+ when Array
507
+ unless framing
508
+ raise JsonLdError::InvalidBaseDirection,
509
+ "@direction value may not be an array unless framing: #{value.inspect}"
510
+ end
511
+ unless value.all? do |v|
512
+ %w[
513
+ ltr rtl
514
+ ].include?(v) || (v.is_a?(Hash) && v.empty?)
515
+ end
516
+ raise JsonLdError::InvalidBaseDirection,
517
+ "@direction must be one of 'ltr', 'rtl', or an array of those if framing #{value.inspect}"
518
+ end
519
+ value
520
+ when Hash
521
+ unless value.empty? && framing
522
+ raise JsonLdError::InvalidBaseDirection,
523
+ "@direction value must be a an empty object for framing: #{value.inspect}"
524
+ end
525
+ [value]
435
526
  else
436
- (framing ? [value] : value)
527
+ raise JsonLdError::InvalidBaseDirection,
528
+ "Value of #{expanded_property} must be one of 'ltr' or 'rtl': #{value.inspect}"
437
529
  end
438
- when Array
439
- raise JsonLdError::InvalidLanguageTaggedString,
440
- "@language value may not be an array unless framing: #{value.inspect}" unless framing
441
- value.each do |v|
442
- if v !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
443
- warn "@language must be valid BCP47: #{v.inspect}"
444
- end
530
+ when '@index'
531
+ # 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.
532
+ unless value.is_a?(String)
533
+ raise JsonLdError::InvalidIndexValue,
534
+ "Value of @index is not a string: #{value.inspect}"
445
535
  end
446
- @options[:lowercaseLanguage] ? value.map(&:downcase) : value
447
- when Hash
448
- raise JsonLdError::InvalidLanguageTaggedString,
449
- "@language value must be a an empty object for framing: #{value.inspect}" unless
450
- value.empty? && framing
451
- [value]
452
- else
453
- raise JsonLdError::InvalidLanguageTaggedString,
454
- "Value of #{expanded_property} must be a string: #{value.inspect}"
455
- end
456
- when '@direction'
457
- # 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.
458
- # If framing, always use array form, unless null
459
- case value
460
- when 'ltr', 'rtl' then (framing ? [value] : value)
461
- when Array
462
- raise JsonLdError::InvalidBaseDirection,
463
- "@direction value may not be an array unless framing: #{value.inspect}" unless framing
464
- raise JsonLdError::InvalidBaseDirection,
465
- "@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?}
466
536
  value
467
- when Hash
468
- raise JsonLdError::InvalidBaseDirection,
469
- "@direction value must be a an empty object for framing: #{value.inspect}" unless
470
- value.empty? && framing
471
- [value]
472
- else
473
- raise JsonLdError::InvalidBaseDirection,
474
- "Value of #{expanded_property} must be one of 'ltr' or 'rtl': #{value.inspect}"
475
- end
476
- when '@index'
477
- # 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.
478
- raise JsonLdError::InvalidIndexValue,
479
- "Value of @index is not a string: #{value.inspect}" unless value.is_a?(String)
480
- value
481
- when '@list'
482
- # If expanded property is @graph:
483
-
484
- # If active property is null or @graph, continue with the next key from element to remove the free-floating list.
485
- next if (expanded_active_property || '@graph') == '@graph'
486
-
487
- # Otherwise, initialize expanded value to the result of using this algorithm recursively passing active context, active property, and value for element.
488
- value = expand(value, active_property, context,
489
- framing: framing,
490
- log_depth: log_depth.to_i + 1)
537
+ when '@list'
538
+ # If expanded property is @graph:
491
539
 
492
- # Spec FIXME: need to be sure that result is an array
493
- value = as_array(value)
540
+ # If active property is null or @graph, continue with the next key from element to remove the free-floating list.
541
+ next if (expanded_active_property || '@graph') == '@graph'
494
542
 
495
- # Make sure that no member of value contains an annotation object
496
- raise JsonLdError::InvalidAnnotation,
497
- "A list element must not contain @annotation." if
498
- value.any? {|n| n.is_a?(Hash) && n.key?('@annotation')}
543
+ # Otherwise, initialize expanded value to the result of using this algorithm recursively passing active context, active property, and value for element.
544
+ value = expand(value, active_property, context,
545
+ framing: framing,
546
+ log_depth: log_depth.to_i + 1)
499
547
 
500
- value
501
- when '@set'
502
- # If expanded property is @set, set expanded value to the result of using this algorithm recursively, passing active context, active property, and value for element.
503
- expand(value, active_property, context,
504
- framing: framing,
505
- log_depth: log_depth.to_i + 1)
506
- when '@reverse'
507
- # If expanded property is @reverse and value is not a JSON object, an invalid @reverse value error has been detected and processing is aborted.
508
- raise JsonLdError::InvalidReverseValue,
509
- "@reverse value must be an object: #{value.inspect}" unless value.is_a?(Hash)
510
-
511
- # Otherwise
512
- # Initialize expanded value to the result of using this algorithm recursively, passing active context, @reverse as active property, and value as element.
513
- value = expand(value, '@reverse', context,
514
- framing: framing,
515
- log_depth: log_depth.to_i + 1)
548
+ # Spec FIXME: need to be sure that result is an array
549
+ value = as_array(value)
516
550
 
517
- # If expanded value contains an @reverse member, i.e., properties that are reversed twice, execute for each of its property and item the following steps:
518
- if value.key?('@reverse')
519
- log_debug("@reverse", depth: log_depth.to_i) {"double reverse: #{value.inspect}"}
520
- value['@reverse'].each do |property, item|
521
- # If result does not have a property member, create one and set its value to an empty array.
522
- # Append item to the value of the property member of result.
523
- (output_object[property] ||= []).concat([item].flatten.compact)
551
+ # Make sure that no member of value contains an annotation object
552
+ if value.any? { |n| n.is_a?(Hash) && n.key?('@annotation') }
553
+ raise JsonLdError::InvalidAnnotation,
554
+ "A list element must not contain @annotation."
555
+ end
556
+
557
+ value
558
+ when '@set'
559
+ # If expanded property is @set, set expanded value to the result of using this algorithm recursively, passing active context, active property, and value for element.
560
+ expand(value, active_property, context,
561
+ framing: framing,
562
+ log_depth: log_depth.to_i + 1)
563
+ when '@reverse'
564
+ # If expanded property is @reverse and value is not a JSON object, an invalid @reverse value error has been detected and processing is aborted.
565
+ unless value.is_a?(Hash)
566
+ raise JsonLdError::InvalidReverseValue,
567
+ "@reverse value must be an object: #{value.inspect}"
568
+ end
569
+
570
+ # Otherwise
571
+ # Initialize expanded value to the result of using this algorithm recursively, passing active context, @reverse as active property, and value as element.
572
+ value = expand(value, '@reverse', context,
573
+ framing: framing,
574
+ log_depth: log_depth.to_i + 1)
575
+
576
+ # If expanded value contains an @reverse member, i.e., properties that are reversed twice, execute for each of its property and item the following steps:
577
+ if value.key?('@reverse')
578
+ # log_debug("@reverse", depth: log_depth.to_i) {"double reverse: #{value.inspect}"}
579
+ value['@reverse'].each do |property, item|
580
+ # If result does not have a property member, create one and set its value to an empty array.
581
+ # Append item to the value of the property member of result.
582
+ (output_object[property] ||= []).concat([item].flatten.compact)
583
+ end
524
584
  end
525
- end
526
585
 
527
- # If expanded value contains members other than @reverse:
528
- if !value.key?('@reverse') || value.length > 1
529
- # If result does not have an @reverse member, create one and set its value to an empty JSON object.
530
- reverse_map = output_object['@reverse'] ||= {}
531
- value.each do |property, items|
532
- next if property == '@reverse'
533
- items.each do |item|
534
- if value?(item) || list?(item)
535
- raise JsonLdError::InvalidReversePropertyValue,
536
- item.inspect
586
+ # If expanded value contains members other than @reverse:
587
+ if !value.key?('@reverse') || value.length > 1
588
+ # If result does not have an @reverse member, create one and set its value to an empty JSON object.
589
+ reverse_map = output_object['@reverse'] ||= {}
590
+ value.each do |property, items|
591
+ next if property == '@reverse'
592
+
593
+ items.each do |item|
594
+ if value?(item) || list?(item)
595
+ raise JsonLdError::InvalidReversePropertyValue,
596
+ item.inspect
597
+ end
598
+ merge_value(reverse_map, property, item)
537
599
  end
538
- merge_value(reverse_map, property, item)
539
600
  end
540
601
  end
602
+
603
+ # Continue with the next key from element
604
+ next
605
+ when '@default', '@embed', '@explicit', '@omitDefault', '@preserve', '@requireAll'
606
+ next unless framing
607
+
608
+ # Framing keywords
609
+ [expand(value, expanded_property, context,
610
+ framing: framing,
611
+ log_depth: log_depth.to_i + 1)].flatten
612
+ when '@nest'
613
+ # Add key to nests
614
+ nests << key
615
+ # Continue with the next key from element
616
+ next
617
+ when '@annotation'
618
+ # Skip unless rdfstar option is set
619
+ next unless @options[:rdfstar]
620
+
621
+ as_array(expand(value, '@annotation', context,
622
+ framing: framing,
623
+ log_depth: log_depth.to_i + 1))
624
+ else
625
+ # Skip unknown keyword
626
+ next
541
627
  end
542
628
 
543
- # Continue with the next key from element
544
- next
545
- when '@default', '@embed', '@explicit', '@omitDefault', '@preserve', '@requireAll'
546
- next unless framing
547
- # Framing keywords
548
- [expand(value, expanded_property, context,
549
- framing: framing,
550
- log_depth: log_depth.to_i + 1)
551
- ].flatten
552
- when '@nest'
553
- # Add key to nests
554
- nests << key
555
- # Continue with the next key from element
556
- next
557
- when '@annotation'
558
- # Skip unless rdfstar option is set
559
- next unless @options[:rdfstar]
560
- as_array(expand(value, '@annotation', context,
561
- framing: framing,
562
- log_depth: log_depth.to_i + 1))
563
- else
564
- # Skip unknown keyword
629
+ # Unless expanded value is null, set the expanded property member of result to expanded value.
630
+ # log_debug("expand #{expanded_property}", depth: log_depth.to_i) { expanded_value.inspect}
631
+ unless expanded_value.nil? && expanded_property == '@value' && input_type != '@json'
632
+ output_object[expanded_property] =
633
+ expanded_value
634
+ end
565
635
  next
566
636
  end
567
637
 
568
- # Unless expanded value is null, set the expanded property member of result to expanded value.
569
- log_debug("expand #{expanded_property}", depth: log_depth.to_i) { expanded_value.inspect}
570
- output_object[expanded_property] = expanded_value unless expanded_value.nil? && expanded_property == '@value' && input_type != '@json'
571
- next
572
- end
638
+ container = context.container(key)
639
+ expanded_value = if context.coerce(key) == '@json'
640
+ # In JSON-LD 1.1, values can be native JSON
641
+ { "@value" => value, "@type" => "@json" }
642
+ elsif container.include?('@language') && value.is_a?(Hash)
643
+ # 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:
573
644
 
574
- container = context.container(key)
575
- expanded_value = if context.coerce(key) == '@json'
576
- # In JSON-LD 1.1, values can be native JSON
577
- {"@value" => value, "@type" => "@json"}
578
- elsif container.include?('@language') && value.is_a?(Hash)
579
- # 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:
580
-
581
- # Set multilingual array to an empty array.
582
- ary = []
583
-
584
- # For each key-value pair language-language value in value, ordered lexicographically by language
585
- keys = @options[:ordered] ? value.keys.sort : value.keys
586
- keys.each do |k|
587
- expanded_k = context.expand_iri(k, vocab: true, as_string: true, base: @options[:base])
588
-
589
- if k !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/ && expanded_k != '@none'
590
- warn "@language must be valid BCP47: #{k.inspect}"
591
- end
645
+ # Set multilingual array to an empty array.
646
+ ary = []
592
647
 
593
- [value[k]].flatten.each do |item|
594
- # item must be a string, otherwise an invalid language map value error has been detected and processing is aborted.
595
- raise JsonLdError::InvalidLanguageMapValue,
596
- "Expected #{item.inspect} to be a string" unless item.nil? || item.is_a?(String)
648
+ # For each key-value pair language-language value in value, ordered lexicographically by language
649
+ keys = @options[:ordered] ? value.keys.sort : value.keys
650
+ keys.each do |k|
651
+ expanded_k = context.expand_iri(k, vocab: true, as_string: true, base: @options[:base])
597
652
 
598
- # Append a JSON object to expanded value that consists of two key-value pairs: (@value-item) and (@language-lowercased language).
599
- v = {'@value' => item}
600
- v['@language'] = (@options[:lowercaseLanguage] ? k.downcase : k) unless expanded_k == '@none'
601
- v['@direction'] = context.direction(key) if context.direction(key)
602
- ary << v if item
603
- end
604
- end
653
+ if k !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/ && expanded_k != '@none'
654
+ warn "@language must be valid BCP47: #{k.inspect}"
655
+ end
605
656
 
606
- ary
607
- elsif container.intersect?(CONTAINER_INDEX_ID_TYPE) && value.is_a?(Hash)
608
- # 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:
609
-
610
- # Set ary to an empty array.
611
- ary = []
612
- index_key = context.term_definitions[key].index || '@index'
613
-
614
- # While processing index keys, if container includes @type, clear type-scoped term definitions
615
- container_context = if container.include?('@type') && context.previous_context
616
- context.previous_context
617
- elsif container.include?('@id') && context.term_definitions[key]
618
- id_context = context.term_definitions[key].context if context.term_definitions[key]
619
- if id_context.nil?
620
- context
621
- else
622
- log_debug("expand", depth: log_depth.to_i) {"id_context: #{id_context.inspect}"}
623
- context.parse(id_context, base: @options[:base], propagate: false)
624
- end
625
- else
626
- context
627
- end
657
+ [value[k]].flatten.each do |item|
658
+ # item must be a string, otherwise an invalid language map value error has been detected and processing is aborted.
659
+ unless item.nil? || item.is_a?(String)
660
+ raise JsonLdError::InvalidLanguageMapValue,
661
+ "Expected #{item.inspect} to be a string"
662
+ end
628
663
 
629
- # For each key-value in the object:
630
- keys = @options[:ordered] ? value.keys.sort : value.keys
631
- keys.each do |k|
632
- # 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
633
- map_context = container_context.term_definitions[k].context if container.include?('@type') && container_context.term_definitions[k]
634
- unless map_context.nil?
635
- log_debug("expand", depth: log_depth.to_i) {"map_context: #{map_context.inspect}"}
636
- map_context = container_context.parse(map_context, base: @options[:base],
637
- propagate: false)
664
+ # Append a JSON object to expanded value that consists of two key-value pairs: (@value-item) and (@language-lowercased language).
665
+ v = { '@value' => item }
666
+ v['@language'] = (@options[:lowercaseLanguage] ? k.downcase : k) unless expanded_k == '@none'
667
+ v['@direction'] = context.direction(key) if context.direction(key)
668
+ ary << v if item
669
+ end
638
670
  end
639
- map_context ||= container_context
640
671
 
641
- expanded_k = container_context.expand_iri(k, vocab: true, as_string: true, base: @options[:base])
672
+ ary
673
+ elsif container.intersect?(CONTAINER_INDEX_ID_TYPE) && value.is_a?(Hash)
674
+ # 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:
675
+
676
+ # Set ary to an empty array.
677
+ ary = []
678
+ index_key = context.term_definitions[key].index || '@index'
679
+
680
+ # While processing index keys, if container includes @type, clear type-scoped term definitions
681
+ container_context = if container.include?('@type') && context.previous_context
682
+ context.previous_context
683
+ elsif container.include?('@id') && context.term_definitions[key]
684
+ id_context = context.term_definitions[key].context if context.term_definitions[key]
685
+ if id_context.nil?
686
+ context
687
+ else
688
+ # log_debug("expand", depth: log_depth.to_i) {"id_context: #{id_context.inspect}"}
689
+ context.parse(id_context, base: @options[:base], propagate: false)
690
+ end
691
+ else
692
+ context
693
+ end
642
694
 
643
- # Initialize index value to the result of using this algorithm recursively, passing active context, key as active property, and index value as element.
644
- index_value = expand([value[k]].flatten, key, map_context,
645
- framing: framing,
646
- from_map: true,
647
- log_depth: log_depth.to_i + 1)
648
- index_value.each do |item|
649
- case
650
- when container.include?('@index')
651
- # Indexed graph by graph name
652
- if !graph?(item) && container.include?('@graph')
653
- item = {'@graph' => as_array(item)}
654
- end
655
- if index_key == '@index'
656
- item['@index'] ||= k unless expanded_k == '@none'
657
- elsif value?(item)
658
- raise JsonLdError::InvalidValueObject, "Attempt to add illegal key to value object: #{index_key}"
659
- else
660
- # Expand key based on term
661
- expanded_k = k == '@none' ? '@none' : container_context.expand_value(index_key, k, base: @options[:base])
662
- index_property = container_context.expand_iri(index_key, vocab: true, as_string: true, base: @options[:base])
663
- item[index_property] = [expanded_k].concat(Array(item[index_property])) unless expanded_k == '@none'
664
- end
665
- when container.include?('@id')
666
- # Indexed graph by graph name
667
- if !graph?(item) && container.include?('@graph')
668
- item = {'@graph' => as_array(item)}
669
- end
670
- # Expand k document relative
671
- expanded_k = container_context.expand_iri(k, as_string: true, base: @options[:base], documentRelative: true) unless expanded_k == '@none'
672
- item['@id'] ||= expanded_k unless expanded_k == '@none'
673
- when container.include?('@type')
674
- item['@type'] = [expanded_k].concat(Array(item['@type'])) unless expanded_k == '@none'
695
+ # For each key-value in the object:
696
+ keys = @options[:ordered] ? value.keys.sort : value.keys
697
+ keys.each do |k|
698
+ # 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
699
+ if container.include?('@type') && container_context.term_definitions[k]
700
+ map_context = container_context.term_definitions[k].context
701
+ end
702
+ unless map_context.nil?
703
+ # log_debug("expand", depth: log_depth.to_i) {"map_context: #{map_context.inspect}"}
704
+ map_context = container_context.parse(map_context, base: @options[:base],
705
+ propagate: false)
675
706
  end
707
+ map_context ||= container_context
708
+
709
+ expanded_k = container_context.expand_iri(k, vocab: true, as_string: true, base: @options[:base])
710
+
711
+ # Initialize index value to the result of using this algorithm recursively, passing active context, key as active property, and index value as element.
712
+ index_value = expand([value[k]].flatten, key, map_context,
713
+ framing: framing,
714
+ from_map: true,
715
+ log_depth: log_depth.to_i + 1)
716
+ index_value.each do |item|
717
+ if container.include?('@index')
718
+ # Indexed graph by graph name
719
+ item = { '@graph' => as_array(item) } if !graph?(item) && container.include?('@graph')
720
+ if index_key == '@index'
721
+ item['@index'] ||= k unless expanded_k == '@none'
722
+ elsif value?(item)
723
+ raise JsonLdError::InvalidValueObject, "Attempt to add illegal key to value object: #{index_key}"
724
+ else
725
+ # Expand key based on term
726
+ expanded_k = if k == '@none'
727
+ '@none'
728
+ else
729
+ container_context.expand_value(index_key, k,
730
+ base: @options[:base])
731
+ end
732
+ index_property = container_context.expand_iri(index_key, vocab: true, as_string: true,
733
+ base: @options[:base])
734
+ item[index_property] = [expanded_k].concat(Array(item[index_property])) unless expanded_k == '@none'
735
+ end
736
+ elsif container.include?('@id')
737
+ # Indexed graph by graph name
738
+ item = { '@graph' => as_array(item) } if !graph?(item) && container.include?('@graph')
739
+ # Expand k document relative
740
+ unless expanded_k == '@none'
741
+ expanded_k = container_context.expand_iri(k, as_string: true, base: @options[:base],
742
+ documentRelative: true)
743
+ end
744
+ item['@id'] ||= expanded_k unless expanded_k == '@none'
745
+ elsif container.include?('@type')
746
+ item['@type'] = [expanded_k].concat(Array(item['@type'])) unless expanded_k == '@none'
747
+ end
676
748
 
677
- # Append item to expanded value.
678
- ary << item
749
+ # Append item to expanded value.
750
+ ary << item
751
+ end
679
752
  end
753
+ ary
754
+ else
755
+ # Otherwise, initialize expanded value to the result of using this algorithm recursively, passing active context, key for active property, and value for element.
756
+ expand(value, key, context,
757
+ framing: framing,
758
+ log_depth: log_depth.to_i + 1)
680
759
  end
681
- ary
682
- else
683
- # Otherwise, initialize expanded value to the result of using this algorithm recursively, passing active context, key for active property, and value for element.
684
- expand(value, key, context,
685
- framing: framing,
686
- log_depth: log_depth.to_i + 1)
687
- end
688
760
 
689
- # If expanded value is null, ignore key by continuing to the next key from element.
690
- if expanded_value.nil?
691
- log_debug(" => skip nil value", depth: log_depth.to_i)
692
- next
693
- end
694
- log_debug(depth: log_depth.to_i) {" => #{expanded_value.inspect}"}
761
+ # If expanded value is null, ignore key by continuing to the next key from element.
762
+ if expanded_value.nil?
763
+ # log_debug(" => skip nil value", depth: log_depth.to_i)
764
+ next
765
+ end
695
766
 
696
- # If the container mapping associated to key in active context is @list and expanded value is not already a list object, convert expanded value to a list object by first setting it to an array containing only expanded value if it is not already an array, and then by setting it to a JSON object containing the key-value pair @list-expanded value.
697
- if container.first == '@list' && container.length == 1 && !list?(expanded_value)
698
- log_debug(" => ", depth: log_depth.to_i) { "convert #{expanded_value.inspect} to list"}
699
- expanded_value = {'@list' => as_array(expanded_value)}
700
- end
701
- log_debug(depth: log_depth.to_i) {" => #{expanded_value.inspect}"}
767
+ # log_debug(depth: log_depth.to_i) {" => #{expanded_value.inspect}"}
702
768
 
703
- # convert expanded value to @graph if container specifies it
704
- if container.first == '@graph' && container.length == 1
705
- log_debug(" => ", depth: log_depth.to_i) { "convert #{expanded_value.inspect} to list"}
706
- expanded_value = as_array(expanded_value).map do |v|
707
- {'@graph' => as_array(v)}
769
+ # If the container mapping associated to key in active context is @list and expanded value is not already a list object, convert expanded value to a list object by first setting it to an array containing only expanded value if it is not already an array, and then by setting it to a JSON object containing the key-value pair @list-expanded value.
770
+ if container.first == '@list' && container.length == 1 && !list?(expanded_value)
771
+ # log_debug(" => ", depth: log_depth.to_i) { "convert #{expanded_value.inspect} to list"}
772
+ expanded_value = { '@list' => as_array(expanded_value) }
708
773
  end
709
- end
774
+ # log_debug(depth: log_depth.to_i) {" => #{expanded_value.inspect}"}
710
775
 
711
- # Otherwise, if the term definition associated to key indicates that it is a reverse property
712
- # Spec FIXME: this is not an otherwise.
713
- if (td = context.term_definitions[key]) && td.reverse_property
714
- # If result has no @reverse member, create one and initialize its value to an empty JSON object.
715
- reverse_map = output_object['@reverse'] ||= {}
716
- [expanded_value].flatten.each do |item|
717
- # If item is a value object or list object, an invalid reverse property value has been detected and processing is aborted.
718
- raise JsonLdError::InvalidReversePropertyValue,
719
- item.inspect if value?(item) || list?(item)
720
-
721
- # If reverse map has no expanded property member, create one and initialize its value to an empty array.
722
- # Append item to the value of the expanded property member of reverse map.
723
- merge_value(reverse_map, expanded_property, item)
776
+ # convert expanded value to @graph if container specifies it
777
+ if container.first == '@graph' && container.length == 1
778
+ # log_debug(" => ", depth: log_depth.to_i) { "convert #{expanded_value.inspect} to list"}
779
+ expanded_value = as_array(expanded_value).map do |v|
780
+ { '@graph' => as_array(v) }
781
+ end
724
782
  end
725
- else
726
- # Otherwise, if key is not a reverse property:
727
- # If result does not have an expanded property member, create one and initialize its value to an empty array.
728
- (output_object[expanded_property] ||= []).tap do |memo|
729
- # expanded_value is either Array[Hash] or Hash; in both case append to memo without flatten
730
- if expanded_value.is_a?(Array)
731
- memo.concat(expanded_value)
732
- else # Hash
733
- memo << expanded_value
783
+
784
+ # Otherwise, if the term definition associated to key indicates that it is a reverse property
785
+ # Spec FIXME: this is not an otherwise.
786
+ if (td = context.term_definitions[key]) && td.reverse_property
787
+ # If result has no @reverse member, create one and initialize its value to an empty JSON object.
788
+ reverse_map = output_object['@reverse'] ||= {}
789
+ [expanded_value].flatten.each do |item|
790
+ # If item is a value object or list object, an invalid reverse property value has been detected and processing is aborted.
791
+ if value?(item) || list?(item)
792
+ raise JsonLdError::InvalidReversePropertyValue,
793
+ item.inspect
794
+ end
795
+
796
+ # If reverse map has no expanded property member, create one and initialize its value to an empty array.
797
+ # Append item to the value of the expanded property member of reverse map.
798
+ merge_value(reverse_map, expanded_property, item)
799
+ end
800
+ else
801
+ # Otherwise, if key is not a reverse property:
802
+ # If result does not have an expanded property member, create one and initialize its value to an empty array.
803
+ (output_object[expanded_property] ||= []).tap do |memo|
804
+ # expanded_value is either Array[Hash] or Hash; in both case append to memo without flatten
805
+ if expanded_value.is_a?(Array)
806
+ memo.concat(expanded_value)
807
+ else # Hash
808
+ memo << expanded_value
809
+ end
734
810
  end
735
811
  end
736
812
  end
737
- end
738
813
 
739
- # For each key in nests, recusively expand content
740
- nests.each do |key|
741
- nest_context = context.term_definitions[key].context if context.term_definitions[key]
742
- nest_context = if nest_context.nil?
743
- context
744
- else
745
- log_debug("expand", depth: log_depth.to_i) {"nest_context: #{nest_context.inspect}"}
746
- context.parse(nest_context, base: @options[:base],
747
- override_protected: true)
748
- end
749
- nested_values = as_array(input[key])
750
- nested_values.each do |nv|
751
- raise JsonLdError::InvalidNestValue, nv.inspect unless
752
- nv.is_a?(Hash) && nv.keys.none? {|k| nest_context.expand_iri(k, vocab: true, base: @options[:base]) == '@value'}
753
- expand_object(nv, active_property, nest_context, output_object,
754
- framing: framing,
755
- expanded_active_property: expanded_active_property,
756
- type_key: type_key,
757
- type_scoped_context: type_scoped_context,
758
- log_depth: log_depth.to_i + 1)
814
+ # For each key in nests, recusively expand content
815
+ nests.each do |key|
816
+ nest_context = context.term_definitions[key].context if context.term_definitions[key]
817
+ nest_context = if nest_context.nil?
818
+ context
819
+ else
820
+ # log_debug("expand", depth: log_depth.to_i) {"nest_context: #{nest_context.inspect}"}
821
+ context.parse(nest_context, base: @options[:base],
822
+ override_protected: true)
823
+ end
824
+ nested_values = as_array(input[key])
825
+ nested_values.each do |nv|
826
+ raise JsonLdError::InvalidNestValue, nv.inspect unless
827
+ nv.is_a?(Hash) && nv.keys.none? do |k|
828
+ nest_context.expand_iri(k, vocab: true, base: @options[:base]) == '@value'
829
+ end
830
+
831
+ expand_object(nv, active_property, nest_context, output_object,
832
+ framing: framing,
833
+ expanded_active_property: expanded_active_property,
834
+ type_key: type_key,
835
+ type_scoped_context: type_scoped_context,
836
+ log_depth: log_depth.to_i + 1)
837
+ end
759
838
  end
760
839
  end
761
840
  end