json-ld 0.3.2 → 0.9.0

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