json-ld 2.2.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,27 +7,42 @@ module RDF
7
7
  Node.new(id + value.to_s)
8
8
  end
9
9
  end
10
- end
11
10
 
12
- class Array
13
- # Sort values, but impose special keyword ordering
14
- # @yield a, b
15
- # @yieldparam [Object] a
16
- # @yieldparam [Object] b
17
- # @yieldreturn [Integer]
18
- # @return [Array]
19
- KW_ORDER = %w(@base @id @value @type @language @vocab @container @graph @list @set @index).freeze
20
- KW_ORDER_CACHE = KW_ORDER.each_with_object({}) do |kw, memo|
21
- memo[kw] = "@#{KW_ORDER.index(kw)}"
22
- end.freeze
11
+ class Statement
12
+ # Validate extended RDF
13
+ def valid_extended?
14
+ has_subject? && subject.resource? && subject.valid_extended? &&
15
+ has_predicate? && predicate.resource? && predicate.valid_extended? &&
16
+ has_object? && object.term? && object.valid_extended? &&
17
+ (has_graph? ? (graph_name.resource? && graph_name.valid_extended?) : true)
18
+ end
19
+ end
23
20
 
24
- # Order, considering keywords to come before other strings
25
- def kw_sort
26
- self.sort do |a, b|
27
- KW_ORDER_CACHE.fetch(a, a) <=> KW_ORDER_CACHE.fetch(b, b)
21
+ class URI
22
+ # Validate extended RDF
23
+ def valid_extended?
24
+ self.valid?
28
25
  end
29
26
  end
30
27
 
28
+ class Node
29
+ # Validate extended RDF
30
+ def valid_extended?
31
+ self.valid?
32
+ end
33
+ end
34
+
35
+ class Literal
36
+ # Validate extended RDF
37
+ def valid_extended?
38
+ return false if language? && language.to_s !~ /^[a-zA-Z]+(-[a-zA-Z0-9]+)*$/
39
+ return false if datatype? && datatype.invalid?
40
+ value.is_a?(String)
41
+ end
42
+ end
43
+ end
44
+
45
+ class Array
31
46
  # Order terms, length first, then lexographically
32
47
  def term_sort
33
48
  self.sort do |a, b|
@@ -7,111 +7,120 @@ module JSON::LD
7
7
  ##
8
8
  # This algorithm creates a JSON object node map holding an indexed representation of the graphs and nodes represented in the passed expanded document. All nodes that are not uniquely identified by an IRI get assigned a (new) blank node identifier. The resulting node map will have a member for every graph in the document whose value is another object with a member for every node represented in the document. The default graph is stored under the @default member, all other graphs are stored under their graph name.
9
9
  #
10
- # @param [Array, Hash] input
10
+ # @param [Array, Hash] element
11
11
  # Expanded JSON-LD input
12
- # @param [Hash] graphs A map of graph name to subjects
13
- # @param [String] graph
12
+ # @param [Hash] graph_map A map of graph name to subjects
13
+ # @param [String] active_graph
14
14
  # The name of the currently active graph that the processor should use when processing.
15
- # @param [String] name
16
- # The name assigned to the current input if it is a bnode
17
- # @param [Array] list
18
- # List to append to, nil for none
19
- def create_node_map(input, graphs, graph: '@default', name: nil, list: nil)
20
- #log_debug("node_map") {"graph: #{graph}, input: #{input.inspect}, name: #{name}"}
21
- case input
22
- when Array
23
- # If input is an array, process each entry in input recursively by passing item for input, node map, active graph, active subject, active property, and list.
24
- input.map {|o| create_node_map(o, graphs, graph: graph, list: list)}
25
- when Hash
26
- type = input['@type']
27
- if value?(input)
28
- # Rename blanknode @type
29
- input['@type'] = namer.get_name(type) if type && blank_node?(type)
30
- list << input if list
31
- else
32
- # Input is a node definition
15
+ # @param [String] active_subject (nil)
16
+ # Node identifier
17
+ # @param [String] active_property (nil)
18
+ # Property within current node
19
+ # @param [Array] list (nil)
20
+ # Used when property value is a list
21
+ def create_node_map(element, graph_map,
22
+ active_graph: '@default',
23
+ active_subject: nil,
24
+ active_property: nil,
25
+ list: nil)
26
+ log_debug("node_map") {"active_graph: #{active_graph}, element: #{element.inspect}, active_subject: #{active_subject}"}
27
+ if element.is_a?(Array)
28
+ # If element is an array, process each entry in element recursively by passing item for element, node map, active graph, active subject, active property, and list.
29
+ element.map do |o|
30
+ create_node_map(o, graph_map,
31
+ active_graph: active_graph,
32
+ active_subject: active_subject,
33
+ active_property: active_property,
34
+ list: list)
35
+ end
36
+ elsif !element.is_a?(Hash)
37
+ raise "Expected hash or array to create_node_map, got #{element.inspect}"
38
+ else
39
+ graph = (graph_map[active_graph] ||= {})
40
+ subject_node = graph[active_subject]
33
41
 
34
- # spec requires @type to be named first, so assign names early
35
- Array(type).each {|t| namer.get_name(t) if blank_node?(t)}
42
+ # Transform BNode types
43
+ if element.has_key?('@type')
44
+ element['@type'] = Array(element['@type']).map {|t| blank_node?(t) ? namer.get_name(t) : t}
45
+ end
36
46
 
37
- # get name for subject
38
- if name.nil?
39
- name ||= input['@id']
40
- name = namer.get_name(name) if blank_node?(name)
47
+ if value?(element)
48
+ element['@type'] = element['@type'].first if element ['@type']
49
+ if list.nil?
50
+ add_value(subject_node, active_property, element, property_is_array: true, allow_duplicate: false)
51
+ else
52
+ list['@list'] << element
41
53
  end
54
+ elsif list?(element)
55
+ result = {'@list' => []}
56
+ create_node_map(element['@list'], graph_map,
57
+ active_graph: active_graph,
58
+ active_subject: active_subject,
59
+ active_property: active_property,
60
+ list: result)
61
+ if list.nil?
62
+ add_value(subject_node, active_property, result, property_is_array: true)
63
+ else
64
+ list['@list'] << result
65
+ end
66
+ else
67
+ # Element is a node object
68
+ id = element.delete('@id')
69
+ id = namer.get_name(id) if blank_node?(id)
42
70
 
43
- # add subject reference to list
44
- list << {'@id' => name} if list
45
-
46
- # create new subject or merge into existing one
47
- subject = (graphs[graph] ||= {})[name] ||= {'@id' => name}
71
+ node = graph[id] ||= {'@id' => id}
48
72
 
49
- input.keys.kw_sort.each do |property|
50
- objects = input[property]
51
- case property
52
- when '@id'
53
- # Skip
54
- when '@reverse'
55
- # handle reverse properties
56
- referenced_node, reverse_map = {'@id' => name}, objects
57
- reverse_map.each do |reverse_property, items|
58
- items.each do |item|
59
- item_name = item['@id']
60
- item_name = namer.get_name(item_name) if blank_node?(item_name)
61
- create_node_map(item, graphs, graph: graph, name: item_name)
62
- add_value(graphs[graph][item_name],
63
- reverse_property,
64
- referenced_node,
65
- property_is_array: true,
66
- allow_duplicate: false)
67
- end
68
- end
69
- when '@graph'
70
- graphs[name] ||= {}
71
- create_node_map(objects, graphs, graph: name)
72
- when /^@(?!type)/
73
- # copy non-@type keywords
74
- if property == '@index' && subject['@index']
75
- raise JsonLdError::ConflictingIndexes,
76
- "Element already has index #{subject['@index']} dfferent from #{input['@index']}" if
77
- subject['@index'] != input['@index']
78
- subject['@index'] = input.delete('@index')
79
- end
80
- subject[property] = objects
73
+ if active_subject.is_a?(Hash)
74
+ # If subject is a hash, then we're processing a reverse-property relationship.
75
+ add_value(node, active_property, active_subject, property_is_array: true, allow_duplicate: false)
76
+ elsif active_property
77
+ reference = {'@id' => id}
78
+ if list.nil?
79
+ add_value(subject_node, active_property, reference, property_is_array: true, allow_duplicate: false)
81
80
  else
82
- # if property is a bnode, assign it a new id
83
- property = namer.get_name(property) if blank_node?(property)
84
-
85
- add_value(subject, property, [], property_is_array: true) if objects.empty?
81
+ list['@list'] << reference
82
+ end
83
+ end
86
84
 
87
- objects.each do |o|
88
- o = namer.get_name(o) if property == '@type' && blank_node?(o)
85
+ if element.has_key?('@type')
86
+ add_value(node, '@type', element.delete('@type'), property_is_array: true, allow_duplicate: false)
87
+ end
89
88
 
90
- case
91
- when node?(o) || node_reference?(o)
92
- id = o['@id']
93
- id = namer.get_name(id) if blank_node?(id)
89
+ if element['@index']
90
+ raise JsonLdError::ConflictingIndexes,
91
+ "Element already has index #{node['@index']} dfferent from #{element['@index']}" if
92
+ node.key?('@index') && node['@index'] != element['@index']
93
+ node['@index'] = element.delete('@index')
94
+ end
94
95
 
95
- # add reference and recurse
96
- add_value(subject, property, {'@id' => id}, property_is_array: true, allow_duplicate: false)
97
- create_node_map(o, graphs, graph: graph, name: id)
98
- when list?(o)
99
- olist = []
100
- create_node_map(o['@list'], graphs, graph: graph, name: name, list: olist)
101
- o = {'@list' => olist}
102
- add_value(subject, property, o, property_is_array: true, allow_duplicate: true)
103
- else
104
- # handle @value
105
- create_node_map(o, graphs, graph: graph, name: name)
106
- add_value(subject, property, o, property_is_array: true, allow_duplicate: false)
107
- end
96
+ if element['@reverse']
97
+ referenced_node, reverse_map = {'@id' => id}, element.delete('@reverse')
98
+ reverse_map.each do |property, values|
99
+ values.each do |value|
100
+ create_node_map(value, graph_map,
101
+ active_graph: active_graph,
102
+ active_subject: referenced_node,
103
+ active_property: property)
108
104
  end
109
105
  end
110
106
  end
107
+
108
+ if element['@graph']
109
+ create_node_map(element.delete('@graph'), graph_map,
110
+ active_graph: id)
111
+ end
112
+
113
+ element.keys.sort.each do |property|
114
+ value = element[property]
115
+
116
+ property = namer.get_name(property) if blank_node?(property)
117
+ node[property] ||= []
118
+ create_node_map(value, graph_map,
119
+ active_graph: active_graph,
120
+ active_subject: id,
121
+ active_property: property)
122
+ end
111
123
  end
112
- else
113
- # add non-object to list
114
- list << input if list
115
124
  end
116
125
  end
117
126
 
@@ -63,12 +63,12 @@ module JSON::LD
63
63
  # If files are empty, either use options[:execute]
64
64
  input = options[:evaluate] ? StringIO.new(options[:evaluate]) : STDIN
65
65
  input.set_encoding(options.fetch(:encoding, Encoding::UTF_8))
66
- JSON::LD::API.expand(input, options.merge(validate: false)) do |expanded|
66
+ JSON::LD::API.expand(input, validate: false, **options) do |expanded|
67
67
  out.puts expanded.to_json(JSON::LD::JSON_STATE)
68
68
  end
69
69
  else
70
70
  files.each do |file|
71
- JSON::LD::API.expand(file, options.merge(validate: false)) do |expanded|
71
+ JSON::LD::API.expand(file, validate: false, **options) do |expanded|
72
72
  out.puts expanded.to_json(JSON::LD::JSON_STATE)
73
73
  end
74
74
  end
@@ -20,13 +20,12 @@ module JSON::LD
20
20
  # @option options [String] :property (nil)
21
21
  # The parent property.
22
22
  # @raise [JSON::LD::InvalidFrame]
23
- def frame(state, subjects, frame, **options)
24
- log_depth do
25
- log_debug("frame") {"subjects: #{subjects.inspect}"}
26
- log_debug("frame") {"frame: #{frame.to_json(JSON_STATE)}"}
27
- log_debug("frame") {"property: #{options[:property].inspect}"}
23
+ def frame(state, subjects, frame, parent: nil, property: nil, **options)
24
+ #log_depth do
25
+ #log_debug("frame") {"subjects: #{subjects.inspect}"}
26
+ #log_debug("frame") {"frame: #{frame.to_json(JSON_STATE)}"}
27
+ #log_debug("frame") {"property: #{property.inspect}"}
28
28
 
29
- parent, property = options[:parent], options[:property]
30
29
  # Validate the frame
31
30
  validate_frame(frame)
32
31
  frame = frame.first if frame.is_a?(Array)
@@ -46,10 +45,10 @@ module JSON::LD
46
45
  matches = filter_subjects(state, subjects, frame, flags)
47
46
 
48
47
  # For each id and node from the set of matched subjects ordered by id
49
- matches.keys.kw_sort.each do |id|
48
+ matches.keys.sort.each do |id|
50
49
  subject = matches[id]
51
50
 
52
- # Note: In order to treat each top-level match as a compartmentalized result, clear the unique embedded subjects map when the property is None, which only occurs at the top-level.
51
+ # Note: In order to treat each top-level match as a compartmentalized result, clear the unique embedded subjects map when the property is nil, which only occurs at the top-level.
53
52
  if property.nil?
54
53
  state[:uniqueEmbeds] = {state[:graph] => {}}
55
54
  else
@@ -105,13 +104,13 @@ module JSON::LD
105
104
  if recurse
106
105
  state[:graphStack].push(state[:graph])
107
106
  state[:graph] = id
108
- frame(state, state[:graphMap][id].keys, [subframe], options.merge(parent: output, property: '@graph'))
107
+ frame(state, state[:graphMap][id].keys, [subframe], parent: output, property: '@graph', **options)
109
108
  state[:graph] = state[:graphStack].pop
110
109
  end
111
110
  end
112
111
 
113
112
  # iterate over subject properties in order
114
- subject.keys.kw_sort.each do |prop|
113
+ subject.keys.sort.each do |prop|
115
114
  objects = subject[prop]
116
115
 
117
116
  # copy keywords to output
@@ -138,14 +137,14 @@ module JSON::LD
138
137
  src = o['@list']
139
138
  src.each do |oo|
140
139
  if node_reference?(oo)
141
- frame(state, [oo['@id']], subframe, options.merge(parent: list, property: '@list'))
140
+ frame(state, [oo['@id']], subframe, parent: list, property: '@list', **options)
142
141
  else
143
142
  add_frame_output(list, '@list', oo.dup)
144
143
  end
145
144
  end
146
145
  when node_reference?(o)
147
146
  # recurse into subject reference
148
- frame(state, [o['@id']], subframe, options.merge(parent: output, property: prop))
147
+ frame(state, [o['@id']], subframe, parent: output, property: prop, **options)
149
148
  when value_match?(subframe, o)
150
149
  # Include values if they match
151
150
  add_frame_output(output, prop, o.dup)
@@ -154,15 +153,14 @@ module JSON::LD
154
153
  end
155
154
 
156
155
  # handle defaults in order
157
- frame.keys.kw_sort.each do |prop|
156
+ frame.keys.sort.each do |prop|
158
157
  next if prop.start_with?('@')
159
158
 
160
159
  # if omit default is off, then include default values for properties that appear in the next frame but are not in the matching subject
161
160
  n = frame[prop].first || {}
162
161
  omit_default_on = get_frame_flag(n, options, :omitDefault)
163
162
  if !omit_default_on && !output[prop]
164
- preserve = n.fetch('@default', '@null').dup
165
- preserve = [preserve] unless preserve.is_a?(Array)
163
+ preserve = as_array(n.fetch('@default', '@null').dup)
166
164
  output[prop] = [{'@preserve' => preserve}]
167
165
  end
168
166
  end
@@ -174,7 +172,7 @@ module JSON::LD
174
172
  # Node has property referencing this subject
175
173
  # recurse into reference
176
174
  (output['@reverse'] ||= {})[reverse_prop] ||= []
177
- frame(state, [r_id], subframe, options.merge(parent: output['@reverse'][reverse_prop]))
175
+ frame(state, [r_id], subframe, parent: output['@reverse'][reverse_prop], property: property, **options)
178
176
  end
179
177
  end
180
178
  end
@@ -185,7 +183,7 @@ module JSON::LD
185
183
  # pop matching subject from circular ref-checking stack
186
184
  state[:subjectStack].pop()
187
185
  end
188
- end
186
+ #end
189
187
  end
190
188
 
191
189
  ##
@@ -213,19 +211,45 @@ module JSON::LD
213
211
  end
214
212
  end
215
213
 
214
+ ##
215
+ # Prune BNode identifiers recursively
216
+ #
217
+ # @param [Array, Hash] input
218
+ # @param [Array<String>] bnodes_to_clear
219
+ # @return [Array, Hash]
220
+ def prune_bnodes(input, bnodes_to_clear)
221
+ result = case input
222
+ when Array
223
+ # If, after replacement, an array contains only the value null remove the value, leaving an empty array.
224
+ input.map {|o| prune_bnodes(o, bnodes_to_clear)}.compact
225
+ when Hash
226
+ output = Hash.new
227
+ input.each do |key, value|
228
+ if context.expand_iri(key) == '@id' && bnodes_to_clear.include?(value)
229
+ # Don't add this to output, as it is pruned as being superfluous
230
+ else
231
+ output[key] = prune_bnodes(value, bnodes_to_clear)
232
+ end
233
+ end
234
+ output
235
+ else
236
+ input
237
+ end
238
+ result
239
+ end
240
+
216
241
  ##
217
242
  # Replace @preserve keys with the values, also replace @null with null.
218
243
  #
219
244
  # Optionally, remove BNode identifiers only used once.
220
245
  #
221
246
  # @param [Array, Hash] input
222
- # @param [Array<String>] bnodes_to_clear
223
247
  # @return [Array, Hash]
224
- def cleanup_preserve(input, bnodes_to_clear)
248
+ def cleanup_preserve(input)
225
249
  result = case input
226
250
  when Array
227
251
  # If, after replacement, an array contains only the value null remove the value, leaving an empty array.
228
- v = input.map {|o| cleanup_preserve(o, bnodes_to_clear)}.compact
252
+ v = input.map {|o| cleanup_preserve(o)}.compact
229
253
 
230
254
  # If the array contains a single member, which is itself an array, use that value as the result
231
255
  (v.length == 1 && v.first.is_a?(Array)) ? v.first : v
@@ -234,11 +258,9 @@ module JSON::LD
234
258
  input.each do |key, value|
235
259
  if key == '@preserve'
236
260
  # replace all key-value pairs where the key is @preserve with the value from the key-pair
237
- output = cleanup_preserve(value, bnodes_to_clear)
238
- elsif context.expand_iri(key) == '@id' && bnodes_to_clear.include?(value)
239
- # Don't add this to output, as it is pruned as being superfluous
261
+ output = cleanup_preserve(value)
240
262
  else
241
- v = cleanup_preserve(value, bnodes_to_clear)
263
+ v = cleanup_preserve(value)
242
264
 
243
265
  # Because we may have added a null value to an array, we need to clean that up, if we possible
244
266
  v = v.first if v.is_a?(Array) && v.length == 1 && !context.as_array?(key)
@@ -275,8 +297,6 @@ module JSON::LD
275
297
  end
276
298
  end
277
299
 
278
- EXCLUDED_FRAMING_KEYWORDS = Set.new(%w(@default @embed @explicit @omitDefault @requireAll)).freeze
279
-
280
300
  ##
281
301
  # Returns true if the given node matches the given frame.
282
302
  #
@@ -319,7 +339,6 @@ module JSON::LD
319
339
  else
320
340
  # Match on specific @type
321
341
  return !(v & node_values).empty?
322
- false
323
342
  end
324
343
  when /@/
325
344
  # Skip other keywords
@@ -329,8 +348,6 @@ module JSON::LD
329
348
  if v = v.first
330
349
  validate_frame(v)
331
350
  has_default = v.has_key?('@default')
332
- # Exclude framing keywords
333
- v = v.reject {|kk,vv| EXCLUDED_FRAMING_KEYWORDS.include?(kk)}
334
351
  end
335
352
 
336
353
 
@@ -348,7 +365,7 @@ module JSON::LD
348
365
  # node does not match if values is not empty and the value of property in frame is match none.
349
366
  return false unless node_values.empty?
350
367
  true
351
- when {}
368
+ when Hash # Empty other than framing keywords
352
369
  # node matches if values is not empty and the value of property in frame is wildcard
353
370
  !node_values.empty?
354
371
  else