json-ld 3.1.4 → 3.1.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,9 +11,9 @@ module JSON::LD
11
11
  # The following constant is used to reduce object allocations
12
12
  CONTAINER_INDEX_ID_TYPE = Set['@index', '@id', '@type'].freeze
13
13
  KEY_ID = %w(@id).freeze
14
- KEYS_VALUE_LANGUAGE_TYPE_INDEX_DIRECTION = %w(@value @language @type @index @direction).freeze
14
+ KEYS_VALUE_LANGUAGE_TYPE_INDEX_DIRECTION = %w(@value @language @type @index @direction @annotation).freeze
15
15
  KEYS_SET_LIST_INDEX = %w(@set @list @index).freeze
16
- KEYS_INCLUDED_TYPE = %w(@included @type).freeze
16
+ KEYS_INCLUDED_TYPE_REVERSE = %w(@included @type @reverse).freeze
17
17
 
18
18
  ##
19
19
  # Expand an Array or Object given an active context and performing local context expansion.
@@ -49,7 +49,13 @@ module JSON::LD
49
49
  log_depth: log_depth.to_i + 1)
50
50
 
51
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
- v = {"@list" => v} if is_list && v.is_a?(Array)
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}
58
+ end
53
59
 
54
60
  case v
55
61
  when nil then nil
@@ -81,7 +87,7 @@ module JSON::LD
81
87
  log_debug("expand", depth: log_depth.to_i) {"after property_scoped_context: #{context.inspect}"} unless property_scoped_context.nil?
82
88
 
83
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.
84
- if input.has_key?('@context')
90
+ if input.key?('@context')
85
91
  context = context.parse(input.delete('@context'), base: @options[:base])
86
92
  log_debug("expand", depth: log_depth.to_i) {"context: #{context.inspect}"}
87
93
  end
@@ -142,13 +148,20 @@ module JSON::LD
142
148
 
143
149
  if output_object['@type'] == '@json' && context.processingMode('json-ld-1.1')
144
150
  # Any value of @value is okay if @type: @json
145
- elsif !ary.all? {|v| v.is_a?(String) || v.is_a?(Hash) && v.empty?} && output_object.has_key?('@language')
151
+ elsif !ary.all? {|v| v.is_a?(String) || v.is_a?(Hash) && v.empty?} && output_object.key?('@language')
146
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.
147
153
  raise JsonLdError::InvalidLanguageTaggedValue,
148
154
  "when @language is used, @value must be a string: #{output_object.inspect}"
149
- elsif !Array(output_object['@type']).all? {|t|
150
- t.is_a?(String) && RDF::URI(t).valid? && !t.start_with?('_:') ||
151
- t.is_a?(Hash) && t.empty?}
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) &&
164
+ RDF::URI(t).valid? && !t.start_with?('_:')
152
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.
153
166
  raise JsonLdError::InvalidTypedValue,
154
167
  "value of @type must be an IRI or '@json': #{output_object.inspect}"
@@ -165,6 +178,18 @@ module JSON::LD
165
178
 
166
179
  # If result contains the key @set, then set result to the key's associated value.
167
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
+
168
193
  end
169
194
 
170
195
  # If result contains only the key @language, set result to null.
@@ -247,10 +272,15 @@ module JSON::LD
247
272
 
248
273
  # If result has already an expanded property member (other than @type), an colliding keywords error has been detected and processing is aborted.
249
274
  raise JsonLdError::CollidingKeywords,
250
- "#{expanded_property} already exists in result" if output_object.has_key?(expanded_property) && !KEYS_INCLUDED_TYPE.include?(expanded_property)
275
+ "#{expanded_property} already exists in result" if output_object.key?(expanded_property) && !KEYS_INCLUDED_TYPE_REVERSE.include?(expanded_property)
251
276
 
252
277
  expanded_value = case expanded_property
253
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
+
254
284
  # If expanded property is @id and value is not a string, an invalid @id value error has been detected and processing is aborted
255
285
  e_id = case value
256
286
  when String
@@ -266,12 +296,27 @@ module JSON::LD
266
296
  context.expand_iri(v, as_string: true, base: @options[:base], documentRelative: true)
267
297
  end
268
298
  when Hash
269
- raise JsonLdError::InvalidIdValue,
270
- "value of @id must be a string unless framing: #{value.inspect}" unless framing
271
- raise JsonLdError::InvalidTypeValue,
272
- "value of @id must be a an empty object for framing: #{value.inspect}" unless
273
- value.empty?
274
- [{}]
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
316
+ else
317
+ raise JsonLdError::InvalidIdValue,
318
+ "value of @id must be a string unless framing: #{value.inspect}" unless framing
319
+ end
275
320
  else
276
321
  raise JsonLdError::InvalidIdValue,
277
322
  "value of @id must be a string or hash if framing: #{value.inspect}"
@@ -447,6 +492,11 @@ module JSON::LD
447
492
  # Spec FIXME: need to be sure that result is an array
448
493
  value = as_array(value)
449
494
 
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')}
499
+
450
500
  value
451
501
  when '@set'
452
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.
@@ -465,7 +515,7 @@ module JSON::LD
465
515
  log_depth: log_depth.to_i + 1)
466
516
 
467
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:
468
- if value.has_key?('@reverse')
518
+ if value.key?('@reverse')
469
519
  #log_debug("@reverse", depth: log_depth.to_i) {"double reverse: #{value.inspect}"}
470
520
  value['@reverse'].each do |property, item|
471
521
  # If result does not have a property member, create one and set its value to an empty array.
@@ -504,6 +554,12 @@ module JSON::LD
504
554
  nests << key
505
555
  # Continue with the next key from element
506
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))
507
563
  else
508
564
  # Skip unknown keyword
509
565
  next
@@ -11,10 +11,10 @@ module RDF
11
11
  class Statement
12
12
  # Validate extended RDF
13
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)
14
+ subject? && subject.resource? && subject.valid_extended? &&
15
+ predicate? && predicate.resource? && predicate.valid_extended? &&
16
+ object? && object.term? && object.valid_extended? &&
17
+ (graph? ? (graph_name.resource? && graph_name.valid_extended?) : true)
18
18
  end
19
19
  end
20
20
 
@@ -1,5 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  # frozen_string_literal: true
3
+ require 'json/canonicalization'
4
+
3
5
  module JSON::LD
4
6
  module Flatten
5
7
  include Utils
@@ -7,6 +9,10 @@ module JSON::LD
7
9
  ##
8
10
  # 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
11
  #
12
+ # For RDF-star/JSON-LD-star:
13
+ # * Values of `@id` can be an object (embedded node); when these are used as keys in a Node Map, they are serialized as canonical JSON, and de-serialized when flattening.
14
+ # * The presence of `@annotation` implies an embedded node and the annotation object is removed from the node/value object in which it appears.
15
+ #
10
16
  # @param [Array, Hash] element
11
17
  # Expanded JSON-LD input
12
18
  # @param [Hash] graph_map A map of graph name to subjects
@@ -16,12 +22,15 @@ module JSON::LD
16
22
  # Node identifier
17
23
  # @param [String] active_property (nil)
18
24
  # Property within current node
25
+ # @param [Boolean] reverse (false)
26
+ # Processing a reverse relationship
19
27
  # @param [Array] list (nil)
20
28
  # Used when property value is a list
21
29
  def create_node_map(element, graph_map,
22
30
  active_graph: '@default',
23
31
  active_subject: nil,
24
32
  active_property: nil,
33
+ reverse: false,
25
34
  list: nil)
26
35
  if element.is_a?(Array)
27
36
  # 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.
@@ -30,21 +39,45 @@ module JSON::LD
30
39
  active_graph: active_graph,
31
40
  active_subject: active_subject,
32
41
  active_property: active_property,
42
+ reverse: false,
33
43
  list: list)
34
44
  end
35
45
  elsif !element.is_a?(Hash)
36
46
  raise "Expected hash or array to create_node_map, got #{element.inspect}"
37
47
  else
38
48
  graph = (graph_map[active_graph] ||= {})
39
- subject_node = graph[active_subject]
49
+ subject_node = !reverse && graph[active_subject.is_a?(Hash) ? active_subject.to_json_c14n : active_subject]
40
50
 
41
51
  # Transform BNode types
42
- if element.has_key?('@type')
52
+ if element.key?('@type')
43
53
  element['@type'] = Array(element['@type']).map {|t| blank_node?(t) ? namer.get_name(t) : t}
44
54
  end
45
55
 
46
56
  if value?(element)
47
57
  element['@type'] = element['@type'].first if element ['@type']
58
+
59
+ # For rdfstar, if value contains an `@annotation` member ...
60
+ # note: active_subject will not be nil, and may be an object itself.
61
+ if element.key?('@annotation')
62
+ # rdfstar being true is implicit, as it is checked in expansion
63
+ as = node_reference?(active_subject) ?
64
+ active_subject['@id'] :
65
+ active_subject
66
+ star_subject = {
67
+ "@id" => as,
68
+ active_property => [element]
69
+ }
70
+
71
+ # Note that annotation is an array, make the reified subject the id of each member of that array.
72
+ annotation = element.delete('@annotation').map do |a|
73
+ a.merge('@id' => star_subject)
74
+ end
75
+
76
+ # Invoke recursively using annotation.
77
+ create_node_map(annotation, graph_map,
78
+ active_graph: active_graph)
79
+ end
80
+
48
81
  if list.nil?
49
82
  add_value(subject_node, active_property, element, property_is_array: true, allow_duplicate: false)
50
83
  else
@@ -64,13 +97,19 @@ module JSON::LD
64
97
  end
65
98
  else
66
99
  # Element is a node object
67
- id = element.delete('@id')
68
- id = namer.get_name(id) if blank_node?(id)
100
+ ser_id = id = element.delete('@id')
101
+ if id.is_a?(Hash)
102
+ # Index graph using serialized id
103
+ ser_id = id.to_json_c14n
104
+ elsif id.nil?
105
+ ser_id = id = namer.get_name
106
+ end
69
107
 
70
- node = graph[id] ||= {'@id' => id}
108
+ node = graph[ser_id] ||= {'@id' => id}
71
109
 
72
- if active_subject.is_a?(Hash)
73
- # If subject is a hash, then we're processing a reverse-property relationship.
110
+ if reverse
111
+ # Note: active_subject is a Hash
112
+ # We're processing a reverse-property relationship.
74
113
  add_value(node, active_property, active_subject, property_is_array: true, allow_duplicate: false)
75
114
  elsif active_property
76
115
  reference = {'@id' => id}
@@ -81,7 +120,30 @@ module JSON::LD
81
120
  end
82
121
  end
83
122
 
84
- if element.has_key?('@type')
123
+ # For rdfstar, if node contains an `@annotation` member ...
124
+ # note: active_subject will not be nil, and may be an object itself.
125
+ # XXX: what if we're reversing an annotation?
126
+ if element.key?('@annotation')
127
+ # rdfstar being true is implicit, as it is checked in expansion
128
+ as = node_reference?(active_subject) ?
129
+ active_subject['@id'] :
130
+ active_subject
131
+ star_subject = reverse ?
132
+ {"@id" => node['@id'], active_property => [{'@id' => as}]} :
133
+ {"@id" => as, active_property => [{'@id' => node['@id']}]}
134
+
135
+ # Note that annotation is an array, make the reified subject the id of each member of that array.
136
+ annotation = element.delete('@annotation').map do |a|
137
+ a.merge('@id' => star_subject)
138
+ end
139
+
140
+ # Invoke recursively using annotation.
141
+ create_node_map(annotation, graph_map,
142
+ active_graph: active_graph,
143
+ active_subject: star_subject)
144
+ end
145
+
146
+ if element.key?('@type')
85
147
  add_value(node, '@type', element.delete('@type'), property_is_array: true, allow_duplicate: false)
86
148
  end
87
149
 
@@ -99,7 +161,8 @@ module JSON::LD
99
161
  create_node_map(value, graph_map,
100
162
  active_graph: active_graph,
101
163
  active_subject: referenced_node,
102
- active_property: property)
164
+ active_property: property,
165
+ reverse: true)
103
166
  end
104
167
  end
105
168
  end
@@ -128,7 +191,72 @@ module JSON::LD
128
191
  end
129
192
  end
130
193
 
194
+ ##
195
+ # Create annotations
196
+ #
197
+ # Updates a node map from which annotations have been folded into embedded triples to re-extract the annotations.
198
+ #
199
+ # Map entries where the key is of the form of a canonicalized JSON object are used to find keys with the `@id` and property components. If found, the original map entry is removed and entries added to an `@annotation` property of the associated value.
200
+ #
201
+ # * Keys which are of the form of a canonicalized JSON object are examined in inverse order of length.
202
+ # * Deserialize the key into a map, and re-serialize the value of `@id`.
203
+ # * If the map contains an entry with that value (after re-canonicalizing, as appropriate), and the associated antry has a item which matches the non-`@id` item from the map, the node is used to create an `@annotation` entry within that value.
204
+ #
205
+ # @param [Hash{String => Hash}] input
206
+ # @return [Hash{String => Hash}]
207
+ def create_annotations(node_map)
208
+ node_map.keys.
209
+ select {|k| k.start_with?('{')}.
210
+ sort_by(&:length).
211
+ reverse.each do |key|
212
+
213
+ annotation = node_map[key]
214
+ # Deserialize key, and re-serialize the `@id` value.
215
+ emb = annotation['@id'].dup
216
+ id = emb.delete('@id')
217
+ property, value = emb.to_a.first
218
+
219
+ # If id is a map, set it to the result of canonicalizing that value, otherwise to itself.
220
+ id = id.to_json_c14n if id.is_a?(Hash)
221
+
222
+ next unless node_map.key?(id)
223
+ # If node map has an entry for id and that entry contains the same property and value from entry:
224
+ node = node_map[id]
225
+
226
+ next unless node.key?(property)
227
+
228
+ node[property].each do |emb_value|
229
+ next unless emb_value == value.first
230
+
231
+ node_map.delete(key)
232
+ annotation.delete('@id')
233
+ add_value(emb_value, '@annotation', annotation, property_is_array: true) unless
234
+ annotation.empty?
235
+ end
236
+ end
237
+ end
238
+
239
+ ##
240
+ # Rename blank nodes recursively within an embedded object
241
+ #
242
+ # @param [Object] node
243
+ # @return [Hash]
244
+ def rename_bnodes(node)
245
+ case node
246
+ when Array
247
+ node.map {|n| rename_bnodes(n)}
248
+ when Hash
249
+ node.inject({}) do |memo, (k, v)|
250
+ v = namer.get_name(v) if k == '@id' && v.is_a?(String) && blank_node?(v)
251
+ memo.merge(k => rename_bnodes(v))
252
+ end
253
+ else
254
+ node
255
+ end
256
+ end
257
+
131
258
  private
259
+
132
260
  ##
133
261
  # Merge nodes from all graphs in the graph_map into a new node map
134
262
  #
@@ -57,10 +57,11 @@ module JSON::LD
57
57
  lambda: ->(files, **options) do
58
58
  out = options[:output] || $stdout
59
59
  out.set_encoding(Encoding::UTF_8) if RUBY_PLATFORM == "java"
60
- options = options.merge(expandContext: options.delete(:context)) if options.has_key?(:context)
60
+ options = options.merge(expandContext: options.delete(:context)) if options.key?(:context)
61
+ options[:base] ||= options[:base_uri]
61
62
  if options[:format] == :jsonld
62
63
  if files.empty?
63
- # If files are empty, either use options[:execute]
64
+ # If files are empty, either use options[:evaluate] or STDIN
64
65
  input = options[:evaluate] ? StringIO.new(options[:evaluate]) : STDIN
65
66
  input.set_encoding(options.fetch(:encoding, Encoding::UTF_8))
66
67
  JSON::LD::API.expand(input, validate: false, **options) do |expanded|
@@ -93,9 +94,10 @@ module JSON::LD
93
94
  raise ArgumentError, "Compacting requires a context" unless options[:context]
94
95
  out = options[:output] || $stdout
95
96
  out.set_encoding(Encoding::UTF_8) if RUBY_PLATFORM == "java"
97
+ options[:base] ||= options[:base_uri]
96
98
  if options[:format] == :jsonld
97
99
  if files.empty?
98
- # If files are empty, either use options[:execute]
100
+ # If files are empty, either use options[:evaluate] or STDIN
99
101
  input = options[:evaluate] ? StringIO.new(options[:evaluate]) : STDIN
100
102
  input.set_encoding(options.fetch(:encoding, Encoding::UTF_8))
101
103
  JSON::LD::API.compact(input, options[:context], **options) do |compacted|
@@ -126,7 +128,7 @@ module JSON::LD
126
128
  control: :url2,
127
129
  use: :required,
128
130
  on: ["--context CONTEXT"],
129
- description: "Context to use when compacting.") {|arg| RDF::URI(arg)},
131
+ description: "Context to use when compacting.") {|arg| RDF::URI(arg).absolute? ? RDF::URI(arg) : StringIO.new(File.read(arg))},
130
132
  ]
131
133
  },
132
134
  flatten: {
@@ -137,9 +139,10 @@ module JSON::LD
137
139
  lambda: ->(files, **options) do
138
140
  out = options[:output] || $stdout
139
141
  out.set_encoding(Encoding::UTF_8) if RUBY_PLATFORM == "java"
142
+ options[:base] ||= options[:base_uri]
140
143
  if options[:format] == :jsonld
141
144
  if files.empty?
142
- # If files are empty, either use options[:execute]
145
+ # If files are empty, either use options[:evaluate] or STDIN
143
146
  input = options[:evaluate] ? StringIO.new(options[:evaluate]) : STDIN
144
147
  input.set_encoding(options.fetch(:encoding, Encoding::UTF_8))
145
148
  JSON::LD::API.flatten(input, options[:context], **options) do |flattened|
@@ -162,7 +165,23 @@ module JSON::LD
162
165
  end
163
166
  end
164
167
  end
165
- end
168
+ end,
169
+ options: [
170
+ RDF::CLI::Option.new(
171
+ symbol: :context,
172
+ datatype: RDF::URI,
173
+ control: :url2,
174
+ use: :required,
175
+ on: ["--context CONTEXT"],
176
+ description: "Context to use when compacting.") {|arg| RDF::URI(arg)},
177
+ RDF::CLI::Option.new(
178
+ symbol: :createAnnotations,
179
+ datatype: TrueClass,
180
+ default: false,
181
+ control: :checkbox,
182
+ on: ["--[no-]create-annotations"],
183
+ description: "Unfold embedded nodes which can be represented using `@annotation`."),
184
+ ]
166
185
  },
167
186
  frame: {
168
187
  description: "Frame JSON-LD or parsed RDF",
@@ -173,9 +192,10 @@ module JSON::LD
173
192
  raise ArgumentError, "Framing requires a frame" unless options[:frame]
174
193
  out = options[:output] || $stdout
175
194
  out.set_encoding(Encoding::UTF_8) if RUBY_PLATFORM == "java"
195
+ options[:base] ||= options[:base_uri]
176
196
  if options[:format] == :jsonld
177
197
  if files.empty?
178
- # If files are empty, either use options[:execute]
198
+ # If files are empty, either use options[:evaluate] or STDIN
179
199
  input = options[:evaluate] ? StringIO.new(options[:evaluate]) : STDIN
180
200
  input.set_encoding(options.fetch(:encoding, Encoding::UTF_8))
181
201
  JSON::LD::API.frame(input, options[:frame], **options) do |framed|
@@ -207,7 +227,7 @@ module JSON::LD
207
227
  control: :url2,
208
228
  use: :required,
209
229
  on: ["--frame FRAME"],
210
- description: "Frame to use when serializing.") {|arg| RDF::URI(arg)}
230
+ description: "Frame to use when serializing.") {|arg| RDF::URI(arg).absolute? ? RDF::URI(arg) : StringIO.new(File.read(arg))}
211
231
  ]
212
232
  },
213
233
  }