json-ld 3.1.3 → 3.1.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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*/JSON-LD*:
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,27 @@ module JSON::LD
128
191
  end
129
192
  end
130
193
 
194
+ ##
195
+ # Rename blank nodes recursively within an embedded object
196
+ #
197
+ # @param [Object] node
198
+ # @return [Hash]
199
+ def rename_bnodes(node)
200
+ case node
201
+ when Array
202
+ node.map {|n| rename_bnodes(n)}
203
+ when Hash
204
+ node.inject({}) do |memo, (k, v)|
205
+ v = namer.get_name(v) if k == '@id' && v.is_a?(String) && blank_node?(v)
206
+ memo.merge(k => rename_bnodes(v))
207
+ end
208
+ else
209
+ node
210
+ end
211
+ end
212
+
131
213
  private
214
+
132
215
  ##
133
216
  # Merge nodes from all graphs in the graph_map into a new node map
134
217
  #
@@ -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,16 @@ 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
+ ]
166
178
  },
167
179
  frame: {
168
180
  description: "Frame JSON-LD or parsed RDF",
@@ -173,9 +185,10 @@ module JSON::LD
173
185
  raise ArgumentError, "Framing requires a frame" unless options[:frame]
174
186
  out = options[:output] || $stdout
175
187
  out.set_encoding(Encoding::UTF_8) if RUBY_PLATFORM == "java"
188
+ options[:base] ||= options[:base_uri]
176
189
  if options[:format] == :jsonld
177
190
  if files.empty?
178
- # If files are empty, either use options[:execute]
191
+ # If files are empty, either use options[:evaluate] or STDIN
179
192
  input = options[:evaluate] ? StringIO.new(options[:evaluate]) : STDIN
180
193
  input.set_encoding(options.fetch(:encoding, Encoding::UTF_8))
181
194
  JSON::LD::API.frame(input, options[:frame], **options) do |framed|
@@ -207,7 +220,7 @@ module JSON::LD
207
220
  control: :url2,
208
221
  use: :required,
209
222
  on: ["--frame FRAME"],
210
- description: "Frame to use when serializing.") {|arg| RDF::URI(arg)}
223
+ description: "Frame to use when serializing.") {|arg| RDF::URI(arg).absolute? ? RDF::URI(arg) : StringIO.new(File.read(arg))}
211
224
  ]
212
225
  },
213
226
  }
data/lib/json/ld/frame.rb CHANGED
@@ -52,7 +52,7 @@ module JSON::LD
52
52
  state[:uniqueEmbeds][state[:graph]] ||= {}
53
53
  end
54
54
 
55
- if flags[:embed] == '@link' && link.has_key?(id)
55
+ if flags[:embed] == '@link' && link.key?(id)
56
56
  # add existing linked subject
57
57
  add_frame_output(parent, property, link[id])
58
58
  next
@@ -66,7 +66,7 @@ module JSON::LD
66
66
  warn "[DEPRECATION] #{flags[:embed]} is not a valid value of @embed in 1.1 mode.\n"
67
67
  end
68
68
 
69
- if !state[:embedded] && state[:uniqueEmbeds][state[:graph]].has_key?(id)
69
+ if !state[:embedded] && state[:uniqueEmbeds][state[:graph]].key?(id)
70
70
  # Skip adding this node object to the top-level, as it was included in another node object
71
71
  next
72
72
  elsif state[:embedded] &&
@@ -76,7 +76,7 @@ module JSON::LD
76
76
  next
77
77
  elsif state[:embedded] &&
78
78
  %w(@first @once).include?(flags[:embed]) &&
79
- state[:uniqueEmbeds][state[:graph]].has_key?(id)
79
+ state[:uniqueEmbeds][state[:graph]].key?(id)
80
80
 
81
81
  # if only the first match should be embedded
82
82
  # Embed unless already embedded
@@ -97,7 +97,7 @@ module JSON::LD
97
97
  state[:subjectStack] << {subject: subject, graph: state[:graph]}
98
98
 
99
99
  # Subject is also the name of a graph
100
- if state[:graphMap].has_key?(id)
100
+ if state[:graphMap].key?(id)
101
101
  # check frame's "@graph" to see what to do next
102
102
  # 1. if it doesn't exist and state.graph === "@merged", don't recurse
103
103
  # 2. if it doesn't exist and state.graph !== "@merged", recurse
@@ -105,7 +105,7 @@ module JSON::LD
105
105
  # 4. if "@default" then don't recurse
106
106
  # 5. recurse
107
107
  recurse, subframe = false, nil
108
- if !frame.has_key?('@graph')
108
+ if !frame.key?('@graph')
109
109
  recurse, subframe = (state[:graph] != '@merged'), {}
110
110
  else
111
111
  subframe = frame['@graph'].first
@@ -134,7 +134,7 @@ module JSON::LD
134
134
  end
135
135
 
136
136
  # explicit is on and property isn't in frame, skip processing
137
- next if flags[:explicit] && !frame.has_key?(prop)
137
+ next if flags[:explicit] && !frame.key?(prop)
138
138
 
139
139
  # add objects
140
140
  objects.each do |o|
@@ -267,7 +267,7 @@ module JSON::LD
267
267
  # If, after replacement, an array contains only the value null remove the value, leaving an empty array.
268
268
  input.map {|o| cleanup_preserve(o)}
269
269
  when Hash
270
- if input.has_key?('@preserve')
270
+ if input.key?('@preserve')
271
271
  # Replace with the content of `@preserve`
272
272
  cleanup_preserve(input['@preserve'].first)
273
273
  else
@@ -388,7 +388,7 @@ module JSON::LD
388
388
  is_empty = v.empty?
389
389
  if v = v.first
390
390
  validate_frame(v)
391
- has_default = v.has_key?('@default')
391
+ has_default = v.key?('@default')
392
392
  end
393
393
 
394
394
  # No longer a wildcard pattern if frame has any non-keyword properties
@@ -14,16 +14,14 @@ module JSON::LD
14
14
  # @param [Boolean] useRdfType (false)
15
15
  # If set to `true`, the JSON-LD processor will treat `rdf:type` like a normal property instead of using `@type`.
16
16
  # @param [Boolean] useNativeTypes (false) use native representations
17
- # @param [Boolean] ordered (true)
18
- # Ensure output objects have keys ordered properly
17
+ #
19
18
  # @return [Array<Hash>] the JSON-LD document in normalized form
20
- def from_statements(dataset, useRdfType: false, useNativeTypes: false, ordered: false)
19
+ def from_statements(dataset, useRdfType: false, useNativeTypes: false)
21
20
  default_graph = {}
22
21
  graph_map = {'@default' => default_graph}
23
22
  referenced_once = {}
24
23
 
25
24
  value = nil
26
- ec = @context
27
25
 
28
26
  # Create an entry for compound-literal node detection
29
27
  compound_literal_subjects = {}
@@ -34,7 +32,7 @@ module JSON::LD
34
32
  dataset.each do |statement|
35
33
  #log_debug("statement") { statement.to_nquads.chomp}
36
34
 
37
- name = statement.graph_name ? ec.expand_iri(statement.graph_name).to_s : '@default'
35
+ name = statement.graph_name ? @context.expand_iri(statement.graph_name, base: @options[:base]).to_s : '@default'
38
36
 
39
37
  # Create a graph entry as needed
40
38
  node_map = graph_map[name] ||= {}
@@ -42,30 +40,29 @@ module JSON::LD
42
40
 
43
41
  default_graph[name] ||= {'@id' => name} unless name == '@default'
44
42
 
45
- subject = ec.expand_iri(statement.subject, as_string: true)
46
- node = node_map[subject] ||= {'@id' => subject}
43
+ subject = statement.subject.to_s
44
+ node = node_map[subject] ||= resource_representation(statement.subject,useNativeTypes)
47
45
 
48
46
  # If predicate is rdf:datatype, note subject in compound literal subjects map
49
47
  if @options[:rdfDirection] == 'compound-literal' && statement.predicate == RDF.to_uri + 'direction'
50
48
  compound_literal_subjects[name][subject] ||= true
51
49
  end
52
50
 
53
- # If object is an IRI or blank node identifier, and node map does not have an object member, create one and initialize its value to a new JSON object consisting of a single member @id whose value is set to object.
54
- node_map[statement.object.to_s] ||= {'@id' => statement.object.to_s} unless
55
- statement.object.literal?
51
+ # If object is an IRI, blank node identifier, or statement, and node map does not have an object member, create one and initialize its value to a new JSON object consisting of a single member @id whose value is set to object.
52
+ unless statement.object.literal?
53
+ node_map[statement.object.to_s] ||=
54
+ resource_representation(statement.object, useNativeTypes)
55
+ end
56
56
 
57
57
  # If predicate equals rdf:type, and object is an IRI or blank node identifier, append object to the value of the @type member of node. If no such member exists, create one and initialize it to an array whose only item is object. Finally, continue to the next RDF triple.
58
+ # XXX JSON-LD* does not support embedded value of @type
58
59
  if statement.predicate == RDF.type && statement.object.resource? && !useRdfType
59
60
  merge_value(node, '@type', statement.object.to_s)
60
61
  next
61
62
  end
62
63
 
63
64
  # Set value to the result of using the RDF to Object Conversion algorithm, passing object, rdfDirection, and use native types.
64
- value = ec.expand_value(nil,
65
- statement.object,
66
- rdfDirection: @options[:rdfDirection],
67
- useNativeTypes: useNativeTypes,
68
- log_depth: @options[:log_depth])
65
+ value = resource_representation(statement.object, useNativeTypes)
69
66
 
70
67
  merge_value(node, statement.predicate.to_s, value)
71
68
 
@@ -78,7 +75,7 @@ module JSON::LD
78
75
  property: statement.predicate.to_s,
79
76
  value: value
80
77
  })
81
- elsif referenced_once.has_key?(statement.object.to_s)
78
+ elsif referenced_once.key?(statement.object.to_s)
82
79
  referenced_once[statement.object.to_s] = false
83
80
  elsif statement.object.node?
84
81
  referenced_once[statement.object.to_s] = {
@@ -147,11 +144,11 @@ module JSON::LD
147
144
  end
148
145
 
149
146
  result = []
150
- default_graph.keys.opt_sort(ordered: ordered).each do |subject|
147
+ default_graph.keys.opt_sort(ordered: @options[:ordered]).each do |subject|
151
148
  node = default_graph[subject]
152
- if graph_map.has_key?(subject)
149
+ if graph_map.key?(subject)
153
150
  node['@graph'] = []
154
- graph_map[subject].keys.opt_sort(ordered: ordered).each do |s|
151
+ graph_map[subject].keys.opt_sort(ordered: @options[:ordered]).each do |s|
155
152
  n = graph_map[subject][s]
156
153
  n.delete(:usages)
157
154
  node['@graph'] << n unless node_reference?(n)
@@ -163,5 +160,31 @@ module JSON::LD
163
160
  #log_debug("fromRdf") {result.to_json(JSON_STATE) rescue 'malformed json'}
164
161
  result
165
162
  end
163
+
164
+ private
165
+ def resource_representation(resource, useNativeTypes)
166
+ case resource
167
+ when RDF::Statement
168
+ # Note, if either subject or object are a BNode which is used elsewhere,
169
+ # this might not work will with the BNode accounting from above.
170
+ rep = {'@id' => resource_representation(resource.subject, false)}
171
+ if resource.predicate == RDF.type
172
+ rep['@id'].merge!('@type' => resource.object.to_s)
173
+ else
174
+ rep['@id'].merge!(
175
+ resource.predicate.to_s =>
176
+ as_array(resource_representation(resource.object, useNativeTypes)))
177
+ end
178
+ rep
179
+ when RDF::Literal
180
+ @context.expand_value(nil,
181
+ resource,
182
+ rdfDirection: @options[:rdfDirection],
183
+ useNativeTypes: useNativeTypes,
184
+ base: @options[:base])
185
+ else
186
+ {'@id' => resource.to_s}
187
+ end
188
+ end
166
189
  end
167
190
  end