json-ld 0.3.2 → 0.9.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.
@@ -47,117 +47,194 @@ module JSON::LD
47
47
  depth do
48
48
  output_object = Hash.ordered
49
49
  # Then, proceed and process each property and value in element as follows:
50
- input.each do |key, value|
51
- # Remove property from element expand property according to the steps outlined in IRI Expansion
52
- property = context.expand_iri(key, :position => :predicate, :quiet => true)
53
-
54
- # Set active property to the original un-expanded property if property if not a keyword
55
- active_property = key unless key[0,1] == '@'
56
- debug("expand property") {"#{active_property.inspect}, expanded: #{property}, value: #{value.inspect}"}
57
-
58
- # If property does not expand to a keyword or absolute IRI, remove property from element
59
- # and continue to the next property from element
60
- if property.nil?
61
- debug(" => ") {"skip nil key"}
50
+ input.keys.kw_sort.each do |property|
51
+ value = input[property]
52
+ expanded_property = context.expand_iri(property, :position => :predicate, :quiet => true, :namer => namer)
53
+
54
+ if expanded_property.is_a?(Array)
55
+ # If expanded property is an array, remove every element which is not a absolute IRI.
56
+ expanded_property = expanded_property.map {|p| p.to_s if p && p.uri? && p.absolute? || p.node?}.compact
57
+ expanded_property = nil if expanded_property.empty?
58
+ elsif expanded_property.is_a?(RDF::Resource)
59
+ expanded_property = expanded_property.to_s
60
+ end
61
+
62
+ debug("expand property") {"ap: #{active_property.inspect}, expanded: #{expanded_property.inspect}, value: #{value.inspect}"}
63
+
64
+ # If expanded property is an empty array, or null, continue with the next property from element
65
+ if expanded_property.nil?
66
+ debug(" => ") {"skip nil property"}
62
67
  next
63
68
  end
64
- property = property.to_s
65
-
66
- expanded_value = case property
67
- when '@id'
68
- # If the property is @id the value must be a string. Expand the value according to IRI Expansion.
69
- context.expand_iri(value, :position => :subject, :quiet => true).to_s
70
- when '@type'
71
- # Otherwise, if the property is @type the value must be a string, an array of strings
72
- # or an empty JSON Object.
73
- # Expand value or each of it's entries according to IRI Expansion
74
- debug("@type") {"value: #{value.inspect}"}
75
- case value
76
- when Array
77
- depth do
78
- [value].flatten.map do |v|
79
- v = v['@id'] if node_reference?(v)
80
- raise ProcessingError, "Object value must be a string or a node reference: #{v.inspect}" unless v.is_a?(String)
81
- context.expand_iri(v, options.merge(:position => :property, :quiet => true)).to_s
69
+ expanded_property
70
+
71
+ if expanded_property.is_a?(String) && expanded_property[0,1] == '@'
72
+ expanded_value = case expanded_property
73
+ when '@id'
74
+ # If expanded property is @id, value must be a string. Set the @id member in result to the result of expanding value according the IRI Expansion algorithm relative to the document base and re-labeling Blank Nodes.
75
+ context.expand_iri(value, :position => :subject, :quiet => true, :namer => namer).to_s
76
+ when '@type'
77
+ # If expanded property is @type, value must be a string or array of strings. Set the @type member of result to the result of expanding value according the IRI Expansion algorithm relative to the document base and re-labeling Blank Nodes, unless that result is an empty array.
78
+ debug("@type") {"value: #{value.inspect}"}
79
+ case value
80
+ when Array
81
+ depth do
82
+ [value].flatten.map do |v|
83
+ v = v['@id'] if node_reference?(v)
84
+ raise ProcessingError, "Object value must be a string or a node reference: #{v.inspect}" unless v.is_a?(String)
85
+ context.expand_iri(v, options.merge(:position => :subject, :quiet => true, :namer => namer)).to_s
86
+ end
87
+ end
88
+ when Hash
89
+ # Empty object used for @type wildcard or node reference
90
+ if node_reference?(value)
91
+ context.expand_iri(value['@id'], options.merge(:position => :property, :quiet => true, :namer => namer)).to_s
92
+ elsif !value.empty?
93
+ raise ProcessingError, "Object value of @type must be empty or a node reference: #{value.inspect}"
94
+ else
95
+ value
82
96
  end
83
- end
84
- when Hash
85
- # Empty object used for @type wildcard or node reference
86
- if node_reference?(value)
87
- context.expand_iri(value['@id'], options.merge(:position => :property, :quiet => true)).to_s
88
- elsif !value.empty?
89
- raise ProcessingError, "Object value of @type must be empty or a node reference: #{value.inspect}"
90
97
  else
91
- value
98
+ context.expand_iri(value, options.merge(:position => :property, :quiet => true, :namer => namer)).to_s
99
+ end
100
+ when '@value'
101
+ # If expanded property is @value, value must be a scalar or null. Set the @value member of result to value.
102
+ raise ProcessingError::Lossy, "Value of #{expanded_property} must be a string, was #{value.inspect}" if value.is_a?(Hash) || value.is_a?(Array)
103
+ value
104
+ when '@language'
105
+ # If expanded property is @language, value must be a string with the lexical form described in [BCP47] or null. Set the @language member of result to the lowercased value.
106
+ raise ProcessingError::Lossy, "Value of #{expanded_property} must be a string, was #{value.inspect}" if value.is_a?(Hash) || value.is_a?(Array)
107
+ value.to_s.downcase
108
+ when '@annotation'
109
+ # If expanded property is @annotation value must be a string. Set the @annotation member of result to value.
110
+ value = value.first if value.is_a?(Array) && value.length == 1
111
+ raise ProcessingError, "Value of @annotation is not a string: #{value.inspect}" unless value.is_a?(String)
112
+ value.to_s
113
+ when '@list', '@set', '@graph'
114
+ # If expanded property is @set, @list, or @graph, set the expanded property member of result to the result of expanding value by recursively using this algorithm, along with the active context and active property. If expanded property is @list and active property is null or @graph, pass @list as active property instead.
115
+ value = [value] unless value.is_a?(Array)
116
+ ap = expanded_property == '@list' && ((active_property || '@graph') == '@graph') ? '@list' : active_property
117
+ value = depth { expand(value, ap, context, options) }
118
+
119
+ # If expanded property is @list, and any expanded value
120
+ # is an object containing an @list property, throw an exception, as lists of lists are not supported
121
+ if expanded_property == '@list' && value.any? {|v| v.is_a?(Hash) && v.has_key?('@list')}
122
+ raise ProcessingError::ListOfLists, "A list may not contain another list"
92
123
  end
124
+
125
+ value
93
126
  else
94
- context.expand_iri(value, options.merge(:position => :property, :quiet => true)).to_s
127
+ # Skip unknown keyword
128
+ next
95
129
  end
96
- when '@value', '@language'
97
- # Otherwise, if the property is @value or @language the value must not be a JSON object or an array.
98
- raise ProcessingError::Lossy, "Value of #{property} must be a string, was #{value.inspect}" if value.is_a?(Hash) || value.is_a?(Array)
99
- value
100
- when '@list', '@set', '@graph'
101
- # Otherwise, if the property is @list, @set, or @graph, expand value recursively
102
- # using this algorithm, passing copies of the active context and active property.
103
- # If the expanded value is not an array, convert it to an array.
104
- value = [value] unless value.is_a?(Array)
105
- value = depth { expand(value, active_property, context, options) }
106
-
107
- # If property is @list, and any expanded value
108
- # is an object containing an @list property, throw an exception, as lists of lists are not supported
109
- if property == '@list' && value.any? {|v| v.is_a?(Hash) && v.has_key?('@list')}
110
- raise ProcessingError::ListOfLists, "A list may not contain another list"
130
+
131
+ debug("expand #{expanded_property}") { expanded_value.inspect}
132
+ output_object[expanded_property] = expanded_value
133
+ next
134
+ end
135
+
136
+ expanded_value = if context.container(property) == '@language' && value.is_a?(Hash)
137
+ # Otherwise, if value is a JSON object and property is not a keyword and its associated term entry in the active context has a @container key associated with a value of @language, process the associated value as a language map:
138
+
139
+ # Set multilingual array to an empty array.
140
+ language_map_values = []
141
+
142
+ # For each key-value in the language map:
143
+ value.keys.sort.each do |k|
144
+ [value[k]].flatten.each do |v|
145
+ # Create a new JSON Object, referred to as an expanded language object.
146
+ expanded_language_object = Hash.new
147
+
148
+ # Add a key-value pair to the expanded language object where the key is @value and the value is the value associated with the key in the language map.
149
+ raise ProcessingError::LanguageMap, "Expected #{vv.inspect} to be a string" unless v.is_a?(String)
150
+ expanded_language_object['@value'] = v
151
+
152
+ # Add a key-value pair to the expanded language object where the key is @language, and the value is the key in the language map, transformed to lowercase.
153
+ # FIXME: check for BCP47 conformance
154
+ expanded_language_object['@language'] = k.downcase
155
+ # Append the expanded language object to the multilingual array.
156
+ language_map_values << expanded_language_object
157
+ end
111
158
  end
159
+ # Set the value associated with property to the multilingual array.
160
+ language_map_values
161
+ elsif context.container(property) == '@annotation' && value.is_a?(Hash)
162
+ # Otherwise, if value is a JSON object and property is not a keyword and its associated term entry in the active context has a @container key associated with a value of @annotation, process the associated value as a annotation:
163
+
164
+ # Set ary to an empty array.
165
+ annotation_map_values = []
112
166
 
113
- value
167
+ # For each key-value in the object:
168
+ value.keys.sort.each do |k|
169
+ [value[k]].flatten.each do |v|
170
+ # Expand the value, adding an '@annotation' key with value equal to the key
171
+ expanded_value = depth { expand(v, property, context, options) }
172
+ next unless expanded_value
173
+ expanded_value['@annotation'] ||= k
174
+ annotation_map_values << expanded_value
175
+ end
176
+ end
177
+ # Set the value associated with property to the multilingual array.
178
+ annotation_map_values
114
179
  else
115
- # Otherwise, expand value recursively using this algorithm, passing copies of the active context and active property.
116
- depth { expand(value, active_property, context, options) }
180
+ # Otherwise, expand value by recursively using this algorithm, passing copies of the active context and property as active property.
181
+ depth { expand(value, property, context, options) }
117
182
  end
118
183
 
119
- # moved from step 2.2.3
120
- # If expanded value is null and property is not @value, continue with the next property
121
- # from element.
122
- if property != '@value' && expanded_value.nil?
184
+ # Continue to the next property-value pair from element if value is null.
185
+ if expanded_value.nil?
123
186
  debug(" => skip nil value")
124
187
  next
125
188
  end
126
189
 
127
- # If the expanded value is not null and property is not a keyword
128
- # and the active property has a @container set to @list,
129
- # convert value to an object with an @list property whose value is set to value
130
- # (unless value is already in that form)
131
- if expanded_value && property[0,1] != '@' && context.container(active_property) == '@list' &&
132
- (!expanded_value.is_a?(Hash) || !expanded_value.fetch('@list', false))
133
- debug(" => ") { "convert #{expanded_value.inspect} to list"}
190
+ # If property's container mapping is set to @list and value is not a JSON object or is a JSON object without a @list member, replace value with a JSON object having a @list member whose value is set to value, ensuring that value is an array.
191
+ if context.container(property) == '@list' &&
192
+ (!expanded_value.is_a?(Hash) || !expanded_value.fetch('@list', false))
193
+
194
+ debug(" => ") { "convert #{expanded_value.inspect} to list"}
134
195
  expanded_value = {'@list' => [expanded_value].flatten}
135
196
  end
136
197
 
137
- # Convert value to array form unless value is null or property is @id, @type, @value, or @language.
138
- if !%(@id @language @type @value).include?(property) && !expanded_value.is_a?(Array)
139
- debug(" => make #{expanded_value.inspect} an array")
140
- expanded_value = [expanded_value]
141
- end
198
+ # Convert value to array form
199
+ debug(" => ") {"expanded property: #{expanded_property.inspect}"}
200
+ expanded_value = [expanded_value] unless expanded_value.is_a?(Array)
142
201
 
143
- if output_object.has_key?(property)
144
- # If element already contains a property property, append value to the existing value.
145
- output_object[property] += expanded_value
202
+ if expanded_property.is_a?(Array)
203
+ label_blanknodes(expanded_value)
204
+ expanded_property.map(&:to_s).each do |prop|
205
+ # label all blank nodes in value with blank node identifiers by using the Label Blank Nodes Algorithm.
206
+ output_object[prop] ||= []
207
+ output_object[prop] += expanded_value.dup
208
+ end
146
209
  else
147
- # Otherwise, create a property property with value as value.
148
- output_object[property] = expanded_value
210
+ if output_object.has_key?(expanded_property)
211
+ # If element already contains a expanded_property property, append value to the existing value.
212
+ output_object[expanded_property] += expanded_value
213
+ else
214
+ # Otherwise, create a property property with value as value.
215
+ output_object[expanded_property] = expanded_value
216
+ end
149
217
  end
150
218
  debug {" => #{expanded_value.inspect}"}
151
219
  end
152
220
 
153
221
  debug("output object") {output_object.inspect}
154
222
 
223
+ # If the active property is null or @graph and element has a @value member without an @annotation member, or element consists of only an @id member, set element to null.
224
+ debug("output object(ap)") {((active_property || '@graph') == '@graph').inspect}
225
+ if (active_property || '@graph') == '@graph' &&
226
+ ((output_object.has_key?('@value') && !output_object.has_key?('@annotation')) ||
227
+ (output_object.keys - %w(@id)).empty?)
228
+ debug("empty top-level") {output_object.inspect}
229
+ return nil
230
+ end
231
+
155
232
  # If the processed element has an @value property
156
233
  if output_object.has_key?('@value')
157
234
  output_object.delete('@language') if output_object['@language'].to_s.empty?
158
235
  output_object.delete('@type') if output_object['@type'].to_s.empty?
159
- if output_object.keys.length > 2 || (%w(@language @type) - output_object.keys).empty?
160
- raise ProcessingError, "element must not have more than one other property, which can either be @language or @type with a string value." unless value.is_a?(String)
236
+ if (%w(@annotation @language @type) - output_object.keys).empty?
237
+ raise ProcessingError, "element must not have more than one other property other than @annotation, which can either be @language or @type with a string value." unless value.is_a?(String)
161
238
  end
162
239
 
163
240
  # if the value of @value equals null, replace element with the value of null.
@@ -168,24 +245,53 @@ module JSON::LD
168
245
  output_object['@type'] = [output_object['@type']]
169
246
  end
170
247
 
171
- # If element has an @set or @list property, it must be the only property. Set element to the value of @set;
248
+ # If element has an @set or @list property, it must be the only property (other tha @annotation). Set element to the value of @set;
172
249
  # leave @list untouched.
173
250
  if !(%w(@set @list) & output_object.keys).empty?
174
- raise ProcessingError, "element must have only @set, @list or @graph" if output_object.keys.length > 1
251
+ o_keys = output_object.keys - %w(@set @list @annotation)
252
+ raise ProcessingError, "element must have only @set or @list: #{output_object.keys.inspect}" if o_keys.length > 1
175
253
 
176
254
  output_object = output_object.values.first unless output_object.has_key?('@list')
177
255
  end
178
256
 
179
- # If element has just a @language property, set element to null.
180
- output_object unless output_object.is_a?(Hash) && output_object.keys == %w(@language)
257
+ # Re-order result keys
258
+ if output_object.is_a?(Hash) && output_object.keys == %w(@language)
259
+ # If element has just a @language property, set element to null.
260
+ nil
261
+ elsif output_object.is_a?(Hash)
262
+ r = Hash.ordered
263
+ output_object.keys.kw_sort.each {|k| r[k] = output_object[k]}
264
+ r
265
+ else
266
+ output_object
267
+ end
181
268
  end
182
269
  else
183
270
  # Otherwise, unless the value is a number, expand the value according to the Value Expansion rules, passing active property.
184
- context.expand_value(active_property, input, :position => :subject, :depth => @depth) unless input.nil?
271
+ context.expand_value(active_property, input,
272
+ :position => :subject, :namer => namer, :depth => @depth
273
+ ) unless input.nil? || active_property.nil? || active_property == '@graph'
185
274
  end
186
275
 
187
276
  debug {" => #{result.inspect}"}
188
277
  result
189
278
  end
279
+
280
+ protected
281
+ # @param [Array, Hash] input
282
+ def label_blanknodes(element)
283
+ if element.is_a?(Array)
284
+ element.each {|e| label_blanknodes(e)}
285
+ elsif list?(element)
286
+ element['@list'].each {|e| label_blanknodes(e)}
287
+ elsif element.is_a?(Hash)
288
+ element.keys.sort.each do |k|
289
+ label_blanknodes(element[k])
290
+ end
291
+ if node?(element) and !element.has_key?('@id')
292
+ element['@id'] = namer.get_name(nil)
293
+ end
294
+ end
295
+ end
190
296
  end
191
- end
297
+ end
@@ -62,6 +62,24 @@ module RDF
62
62
  end
63
63
  end
64
64
 
65
+ class Array
66
+ # Sort values, but impose special keyword ordering
67
+ # @yield a, b
68
+ # @yieldparam [Object] a
69
+ # @yieldparam [Object] b
70
+ # @yieldreturn [Integer]
71
+ # @return [Array]
72
+ KW_ORDER = %(@id @value @type @language @vocab @container @graph @list @set)
73
+
74
+ def kw_sort
75
+ self.sort do |a, b|
76
+ a = "@#{KW_ORDER.index(a)}" if KW_ORDER.include?(a)
77
+ b = "@#{KW_ORDER.index(b)}" if KW_ORDER.include?(b)
78
+ a <=> b
79
+ end
80
+ end
81
+ end
82
+
65
83
  if RUBY_VERSION < "1.9"
66
84
  class InsertOrderPreservingHash < Hash
67
85
  include Enumerable
@@ -13,14 +13,13 @@ module JSON::LD
13
13
  # Graph name for results
14
14
  # @param [Array] list
15
15
  # List for saving list elements
16
- # @param [BlankNodeNamer] namer
17
- # @param [String] id
16
+ # @param [String] id (nil)
18
17
  # Identifier already associated with element
19
- def generate_node_map(element, node_map, graph, list, namer, id = nil)
18
+ def generate_node_map(element, node_map, graph, list = nil, id = nil)
20
19
  depth do
21
20
  debug("nodeMap") {"element: #{element.inspect}, graph: #{graph}"}
22
21
  if element.is_a?(Array)
23
- element.map {|o| generate_node_map(o, node_map, graph, list, namer)}
22
+ element.map {|o| generate_node_map(o, node_map, graph, list)}
24
23
  elsif !element.is_a?(Hash) || value?(element)
25
24
  list << element if list
26
25
  else
@@ -48,7 +47,7 @@ module JSON::LD
48
47
  when '@graph'
49
48
  # If property is @graph, recursively call this algorithm passing value for element, nodeMap, nil for list and if graph is @merged use graph, otherwise use id for graph and then continue.
50
49
  graph = graph == '@merged' ? '@merged' : id
51
- generate_node_map(value, node_map, graph, nil, namer)
50
+ generate_node_map(value, node_map, graph)
52
51
  when /^@(?!type)/
53
52
  # If property is not @type and is a keyword, merge property and value into node and then continue.
54
53
  debug("nodeMap") {"merge keyword#{prop}: #{value.inspect}"}
@@ -75,7 +74,7 @@ module JSON::LD
75
74
  }
76
75
 
77
76
  # Recursively call this algorithm passing v for value, nodeMap, graph, and nil for list.
78
- generate_node_map(v, node_map, graph, nil, namer, name)
77
+ generate_node_map(v, node_map, graph, nil, name)
79
78
  elsif list?(v)
80
79
  # Otherwise if v has the property @list then recursively call this algorithm with the value of @list as element, nodeMap, graph, and a new array flattenedList as list.
81
80
  debug("nodeMap") {"list value #{prop}: #{v.inspect}"}
@@ -83,8 +82,7 @@ module JSON::LD
83
82
  generate_node_map(v['@list'],
84
83
  node_map,
85
84
  graph,
86
- flattened_list,
87
- namer)
85
+ flattened_list)
88
86
  # Create a new JSON object with the property @list set to flattenedList and add it to node for property.
89
87
  (node[prop] ||= []) << {'@list' => flattened_list}
90
88
  elsif prop == '@type'
data/lib/json/ld/frame.rb CHANGED
@@ -36,7 +36,7 @@ module JSON::LD
36
36
  debug("frame") {"embed: #{embed.inspect}, explicit: #{explicit.inspect}"}
37
37
 
38
38
  # For each id and node from the set of matched nodes ordered by id
39
- matches.keys.sort.each do |id|
39
+ matches.keys.kw_sort.each do |id|
40
40
  element = matches[id]
41
41
  # If the active property is null, set the map of embeds in state to an empty map
42
42
  state = state.merge(:embeds => {}) if property.nil?
@@ -77,7 +77,7 @@ module JSON::LD
77
77
  debug("frame") {"add embedded_node: #{embedded_node.inspect}"}
78
78
 
79
79
  # Process each property and value in the matched node as follows
80
- element.keys.sort.each do |prop|
80
+ element.keys.kw_sort.each do |prop|
81
81
  value = element[prop]
82
82
  if prop[0,1] == '@'
83
83
  # If property is a keyword, add property and a copy of value to output and continue with the next property from node
@@ -138,7 +138,7 @@ module JSON::LD
138
138
  end
139
139
 
140
140
  # Process each property and value in frame in lexographical order, where property is not a keyword, as follows:
141
- frame.keys.sort.each do |prop|
141
+ frame.keys.kw_sort.each do |prop|
142
142
  next if prop[0,1] == '@' || output.has_key?(prop)
143
143
  property_frame = frame[prop]
144
144
  debug("frame") {"frame prop: #{prop.inspect}. property_frame: #{property_frame.inspect}"}
@@ -1,3 +1,4 @@
1
+ require 'rdf'
1
2
  require 'rdf/nquads'
2
3
 
3
4
  module JSON::LD
@@ -21,7 +22,6 @@ module JSON::LD
21
22
  # @yieldparam [RDF::Statement] statement
22
23
  def statements(path, element, subject, property, name, &block)
23
24
  debug(path) {"statements: e=#{element.inspect}, s=#{subject.inspect}, p=#{property.inspect}, n=#{name.inspect}"}
24
- @node_seq = "t0" unless subject || property
25
25
 
26
26
  traverse_result = depth do
27
27
  case element
@@ -55,7 +55,7 @@ module JSON::LD
55
55
 
56
56
  # 1.7) For each key in the JSON object that has not already been processed,
57
57
  # perform the following steps:
58
- element.keys.sort.each do |key|
58
+ element.keys.kw_sort.each do |key|
59
59
  value = element[key]
60
60
  active_property = case key
61
61
  when '@type'
@@ -65,7 +65,7 @@ module JSON::LD
65
65
  # Otherwise, if property is @graph, process value algorithm recursively, using active subject
66
66
  # as graph name and null values for active subject and active property and then continue to
67
67
  # next property
68
- statements("#{path}[#{key}]", value, nil, nil, active_subject, &block)
68
+ statements("#{path}[#{key}]", value, nil, nil, (active_subject unless active_subject.node?), &block)
69
69
  next
70
70
  when /^@/
71
71
  # Otherwise, if property is a keyword, skip this step.
@@ -154,9 +154,7 @@ module JSON::LD
154
154
  ##
155
155
  # Create a new named node using the sequence
156
156
  def node
157
- n = RDF::Node.new(@node_seq)
158
- @node_seq = @node_seq.succ
159
- n
157
+ RDF::Node.new(namer.get_sym)
160
158
  end
161
159
 
162
160
  ##