json-ld 2.1.0 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -68,8 +68,7 @@ module JSON::LD
68
68
  end
69
69
  when '@graph'
70
70
  graphs[name] ||= {}
71
- g = graph == '@merged' ? graph : name
72
- create_node_map(objects, graphs, graph: g)
71
+ create_node_map(objects, graphs, graph: name)
73
72
  when /^@(?!type)/
74
73
  # copy non-@type keywords
75
74
  if property == '@index' && subject['@index']
@@ -115,5 +114,35 @@ module JSON::LD
115
114
  list << input if list
116
115
  end
117
116
  end
117
+
118
+ private
119
+ ##
120
+ # Merge nodes from all graphs in the graph_map into a new node map
121
+ #
122
+ # @param [Hash{String => Hash}] graph_map
123
+ # @return [Hash]
124
+ def merge_node_map_graphs(graph_map)
125
+ merged = {}
126
+ graph_map.each do |name, node_map|
127
+ node_map.each do |id, node|
128
+ merged_node = (merged[id] ||= {'@id' => id})
129
+
130
+ # Iterate over node properties
131
+ node.each do |property, values|
132
+ if property.start_with?('@')
133
+ # Copy keywords
134
+ merged_node[property] = node[property].dup
135
+ else
136
+ # Merge objects
137
+ values.each do |value|
138
+ add_value(merged_node, property, value.dup, property_is_array: true)
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ merged
146
+ end
118
147
  end
119
148
  end
@@ -62,12 +62,12 @@ module JSON::LD
62
62
  # If files are empty, either use options[:execute]
63
63
  input = options[:evaluate] ? StringIO.new(options[:evaluate]) : STDIN
64
64
  input.set_encoding(options.fetch(:encoding, Encoding::UTF_8))
65
- JSON::LD::API.expand(input, options) do |expanded|
65
+ JSON::LD::API.expand(input, options.merge(validate: false)) do |expanded|
66
66
  out.puts expanded.to_json(JSON::LD::JSON_STATE)
67
67
  end
68
68
  else
69
69
  files.each do |file|
70
- JSON::LD::API.expand(file, options) do |expanded|
70
+ JSON::LD::API.expand(file, options.merge(validate: false)) do |expanded|
71
71
  out.puts expanded.to_json(JSON::LD::JSON_STATE)
72
72
  end
73
73
  end
@@ -152,9 +152,9 @@ module JSON::LD
152
152
  end
153
153
  },
154
154
  frame: {
155
- description: "Flatten JSON-LD or parsed RDF",
155
+ description: "Frame JSON-LD or parsed RDF",
156
156
  parse: false,
157
- help: "flatten --frame <frame-file> files ...",
157
+ help: "frame --frame <frame-file> files ...",
158
158
  lambda: ->(files, options) do
159
159
  raise ArgumentError, "Framing requires a frame" unless options[:frame]
160
160
  out = options[:output] || $stdout
@@ -18,10 +18,15 @@ module JSON::LD
18
18
  # @option options [String] :property (nil)
19
19
  # The parent property.
20
20
  # @raise [JSON::LD::InvalidFrame]
21
- def frame(state, subjects, frame, options = {})
21
+ def frame(state, subjects, frame, **options)
22
+ log_depth do
23
+ log_debug("frame") {"subjects: #{subjects.inspect}"}
24
+ log_debug("frame") {"frame: #{frame.to_json(JSON_STATE)}"}
25
+ log_debug("frame") {"property: #{options[:property].inspect}"}
26
+
22
27
  parent, property = options[:parent], options[:property]
23
28
  # Validate the frame
24
- validate_frame(state, frame)
29
+ validate_frame(frame)
25
30
  frame = frame.first if frame.is_a?(Array)
26
31
 
27
32
  # Get values for embedOn and explicitOn
@@ -31,6 +36,9 @@ module JSON::LD
31
36
  requireAll: get_frame_flag(frame, options, :requireAll),
32
37
  }
33
38
 
39
+ # Get link for current graph
40
+ link = state[:link][state[:graph]] ||= {}
41
+
34
42
  # Create a set of matched subjects by filtering subjects by checking the map of flattened subjects against frame
35
43
  # This gives us a hash of objects indexed by @id
36
44
  matches = filter_subjects(state, subjects, frame, flags)
@@ -39,25 +47,24 @@ module JSON::LD
39
47
  matches.keys.kw_sort.each do |id|
40
48
  subject = matches[id]
41
49
 
42
- if flags[:embed] == '@link' && state[:link].has_key?(id)
43
- # TODO: may want to also match an existing linked subject
44
- # against the current frame ... so different frames could
45
- # produce different subjects that are only shared in-memory
46
- # when the frames are the same
50
+ # 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
+ if property.nil?
52
+ state[:uniqueEmbeds] = {state[:graph] => {}}
53
+ else
54
+ state[:uniqueEmbeds][state[:graph]] ||= {}
55
+ end
47
56
 
57
+ if flags[:embed] == '@link' && link.has_key?(id)
48
58
  # add existing linked subject
49
- add_frame_output(parent, property, state[:link][id])
59
+ add_frame_output(parent, property, link[id])
50
60
  next
51
61
  end
52
62
 
53
- # 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.
54
- state = state.merge(uniqueEmbeds: {}) if property.nil?
55
-
56
63
  output = {'@id' => id}
57
- state[:link][id] = output
64
+ link[id] = output
58
65
 
59
66
  # if embed is @never or if a circular reference would be created by an embed, the subject cannot be embedded, just add the reference; note that a circular reference won't occur when the embed flag is `@link` as the above check will short-circuit before reaching this point
60
- if flags[:embed] == '@never' || creates_circular_reference(subject, state[:subjectStack])
67
+ if flags[:embed] == '@never' || creates_circular_reference(subject, state[:graph], state[:subjectStack])
61
68
  add_frame_output(parent, property, output)
62
69
  next
63
70
  end
@@ -65,15 +72,40 @@ module JSON::LD
65
72
  # if only the last match should be embedded
66
73
  if flags[:embed] == '@last'
67
74
  # remove any existing embed
68
- remove_embed(state, id) if state[:uniqueEmbeds].include?(id)
69
- state[:uniqueEmbeds][id] = {
75
+ remove_embed(state, id) if state[:uniqueEmbeds][state[:graph]].include?(id)
76
+ state[:uniqueEmbeds][state[:graph]][id] = {
70
77
  parent: parent,
71
78
  property: property
72
79
  }
73
80
  end
74
81
 
75
82
  # push matching subject onto stack to enable circular embed checks
76
- state[:subjectStack] << subject
83
+ state[:subjectStack] << {subject: subject, graph: state[:graph]}
84
+
85
+ # Subject is also the name of a graph
86
+ if state[:graphMap].has_key?(id)
87
+ # check frame's "@graph" to see what to do next
88
+ # 1. if it doesn't exist and state.graph === "@merged", don't recurse
89
+ # 2. if it doesn't exist and state.graph !== "@merged", recurse
90
+ # 3. if "@merged" then don't recurse
91
+ # 4. if "@default" then don't recurse
92
+ # 5. recurse
93
+ recurse, subframe = false, nil
94
+ if !frame.has_key?('@graph')
95
+ recurse, subframe = (state[:graph] != '@merged'), {}
96
+ else
97
+ subframe = frame['@graph'].first
98
+ recurse = !%w(@merged @default).include?(subframe)
99
+ subframe = {} unless subframe.is_a?(Hash)
100
+ end
101
+
102
+ if recurse
103
+ state[:graphStack].push(state[:graph])
104
+ state[:graph] = id
105
+ frame(state, state[:graphMap][id].keys, [subframe], options.merge(parent: output, property: '@graph'))
106
+ state[:graph] = state[:graphStack].pop
107
+ end
108
+ end
77
109
 
78
110
  # iterate over subject properties in order
79
111
  subject.keys.kw_sort.each do |prop|
@@ -90,8 +122,12 @@ module JSON::LD
90
122
 
91
123
  # add objects
92
124
  objects.each do |o|
125
+ subframe = Array(frame[prop]).first || create_implicit_frame(flags)
126
+
93
127
  case
94
128
  when list?(o)
129
+ subframe = frame[prop].first['@list'] if Array(frame[prop]).first.is_a?(Hash)
130
+ subframe ||= create_implicit_frame(flags)
95
131
  # add empty list
96
132
  list = {'@list' => []}
97
133
  add_frame_output(output, prop, list)
@@ -99,8 +135,6 @@ module JSON::LD
99
135
  src = o['@list']
100
136
  src.each do |oo|
101
137
  if node_reference?(oo)
102
- subframe = frame[prop].first['@list'] if frame[prop].is_a?(Array) && frame[prop].first.is_a?(Hash)
103
- subframe ||= create_implicit_frame(flags)
104
138
  frame(state, [oo['@id']], subframe, options.merge(parent: list, property: '@list'))
105
139
  else
106
140
  add_frame_output(list, '@list', oo.dup)
@@ -108,10 +142,9 @@ module JSON::LD
108
142
  end
109
143
  when node_reference?(o)
110
144
  # recurse into subject reference
111
- subframe = frame[prop] || create_implicit_frame(flags)
112
145
  frame(state, [o['@id']], subframe, options.merge(parent: output, property: prop))
113
- else
114
- # include other values automatically
146
+ when value_match?(subframe, o)
147
+ # Include values if they match
115
148
  add_frame_output(output, prop, o.dup)
116
149
  end
117
150
  end
@@ -147,6 +180,7 @@ module JSON::LD
147
180
  # pop matching subject from circular ref-checking stack
148
181
  state[:subjectStack].pop()
149
182
  end
183
+ end
150
184
  end
151
185
 
152
186
  ##
@@ -191,7 +225,7 @@ module JSON::LD
191
225
  #
192
226
  # @param [Hash{Symbol => Object}] state
193
227
  # Current framing state
194
- # @param [Hash{String => Hash}] subjects
228
+ # @param [Array<String>] subjects
195
229
  # The subjects to filter
196
230
  # @param [Hash{String => Object}] frame
197
231
  # @param [Hash{Symbol => String}] flags the frame flags.
@@ -199,8 +233,8 @@ module JSON::LD
199
233
  # @return all of the matched subjects.
200
234
  def filter_subjects(state, subjects, frame, flags)
201
235
  subjects.inject({}) do |memo, id|
202
- subject = state[:subjects][id]
203
- memo[id] = subject if filter_subject(subject, frame, flags)
236
+ subject = state[:graphMap][state[:graph]][id]
237
+ memo[id] = subject if filter_subject(subject, frame, state, flags)
204
238
  memo
205
239
  end
206
240
  end
@@ -214,54 +248,113 @@ module JSON::LD
214
248
  #
215
249
  # @param [Hash{String => Object}] subject the subject to check.
216
250
  # @param [Hash{String => Object}] frame the frame to check.
251
+ # @param [Hash{Symbol => Object}] state Current framing state
217
252
  # @param [Hash{Symbol => Object}] flags the frame flags.
218
253
  #
219
254
  # @return [Boolean] true if the node matches, false if not.
220
- def filter_subject(subject, frame, flags)
221
- types = frame.fetch('@type', [])
222
- raise InvalidFrame::Syntax, "frame @type must be an array: #{types.inspect}" unless types.is_a?(Array)
223
- subject_types = subject.fetch('@type', [])
224
- raise InvalidFrame::Syntax, "node @type must be an array: #{node_types.inspect}" unless subject_types.is_a?(Array)
225
-
226
- # check @type (object value means 'any' type, fall through to ducktyping)
227
- if !types.empty? &&
228
- !(types.length == 1 && types.first.is_a?(Hash))
229
- # If frame has an @type, use it for selecting appropriate nodes.
230
- return types.any? {|t| subject_types.include?(t)}
231
- else
232
- # Duck typing, for nodes not having a type, but having @id
233
- wildcard, matches_some = true, false
234
-
235
- frame.each do |k, v|
236
- case k
237
- when '@id'
238
- return false if v.is_a?(String) && subject['@id'] != v
239
- wildcard, matches_some = false, true
240
- when '@type'
241
- wildcard, matches_some = false, false
242
- when /^@/
255
+ def filter_subject(subject, frame, state, flags)
256
+ # Duck typing, for nodes not having a type, but having @id
257
+ wildcard, matches_some = true, false
258
+
259
+ frame.each do |k, v|
260
+ node_values = subject.fetch(k, [])
261
+
262
+ case k
263
+ when '@id'
264
+ ids = v || []
265
+
266
+ # Match on specific @id.
267
+ return ids.include?(subject['@id']) if !ids.empty? && ids != [{}]
268
+ match_this = true
269
+ when '@type'
270
+ # No longer a wildcard pattern
271
+ wildcard = false
272
+
273
+ match_this = case v
274
+ when []
275
+ # Don't Match on no @type
276
+ return false if !node_values.empty?
277
+ true
278
+ when [{}]
279
+ # Match on wildcard @type
280
+ !node_values.empty?
243
281
  else
244
- wildcard = false
282
+ # Match on specific @type
283
+ return !(v & node_values).empty?
284
+ false
285
+ end
286
+ when /@/
287
+ # Skip other keywords
288
+ next
289
+ else
290
+ is_empty = v.empty?
291
+ if v = v.first
292
+ validate_frame(v)
293
+ has_default = v.has_key?('@default')
294
+ # Exclude framing keywords
295
+ v = v.dup.delete_if {|kk,vv| %w(@default @embed @explicit @omitDefault @requireAll).include?(kk)}
296
+ end
245
297
 
246
- # v == [] means do not match if property is present
247
- if subject.has_key?(k)
248
- return false if v == [] && !subject[k].nil?
249
- matches_some = true
250
- next
251
- end
252
298
 
253
- # all properties must match to be a duck unless a @default is specified
254
- has_default = v.is_a?(Array) && v.length == 1 && v.first.is_a?(Hash) && v.first.has_key?('@default')
255
- return false if flags[:requireAll] && !has_default
299
+ # No longer a wildcard pattern if frame has any non-keyword properties
300
+ wildcard = false
301
+
302
+ # Skip, but allow match if node has no value for property, and frame has a default value
303
+ next if node_values.empty? && has_default
304
+
305
+ # If frame value is empty, don't match if subject has any value
306
+ return false if !node_values.empty? && is_empty
307
+
308
+ match_this = case v
309
+ when nil
310
+ # node does not match if values is not empty and the value of property in frame is match none.
311
+ return false unless node_values.empty?
312
+ true
313
+ when {}
314
+ # node matches if values is not empty and the value of property in frame is wildcard
315
+ !node_values.empty?
316
+ else
317
+ if value?(v)
318
+ # Match on any matching value
319
+ node_values.any? {|nv| value_match?(v, nv)}
320
+ elsif node?(v) || node_reference?(v)
321
+ node_values.any? do |nv|
322
+ node_match?(v, nv, state, flags)
323
+ end
324
+ elsif list?(v)
325
+ vv = v['@list'].first
326
+ node_values = list?(node_values.first) ?
327
+ node_values.first['@list'] :
328
+ false
329
+ if !node_values
330
+ false # Lists match Lists
331
+ elsif value?(vv)
332
+ # Match on any matching value
333
+ node_values.any? {|nv| value_match?(vv, nv)}
334
+ elsif node?(vv) || node_reference?(vv)
335
+ node_values.any? do |nv|
336
+ node_match?(vv, nv, state, flags)
337
+ end
338
+ else
339
+ false
340
+ end
341
+ else
342
+ false # No matching on non-value or node values
343
+ end
256
344
  end
257
345
  end
258
346
 
259
- # return true if wildcard or subject matches some properties
260
- wildcard || matches_some
347
+ # All non-defaulted values must match if @requireAll is set
348
+ return false if !match_this && flags[:requireAll]
349
+
350
+ matches_some ||= match_this
261
351
  end
352
+
353
+ # return true if wildcard or subject matches some properties
354
+ wildcard || matches_some
262
355
  end
263
356
 
264
- def validate_frame(state, frame)
357
+ def validate_frame(frame)
265
358
  raise InvalidFrame::Syntax,
266
359
  "Invalid JSON-LD syntax; a JSON-LD frame must be an object: #{frame.inspect}" unless
267
360
  frame.is_a?(Hash) || (frame.is_a?(Array) && frame.first.is_a?(Hash) && frame.length == 1)
@@ -270,12 +363,13 @@ module JSON::LD
270
363
  # Checks the current subject stack to see if embedding the given subject would cause a circular reference.
271
364
  #
272
365
  # @param subject_to_embed the subject to embed.
366
+ # @param graph the graph the subject to embed is in.
273
367
  # @param subject_stack the current stack of subjects.
274
368
  #
275
369
  # @return true if a circular reference would be created, false if not.
276
- def creates_circular_reference(subject_to_embed, subject_stack)
370
+ def creates_circular_reference(subject_to_embed, graph, subject_stack)
277
371
  subject_stack[0..-2].any? do |subject|
278
- subject['@id'] == subject_to_embed['@id']
372
+ subject[:graph] == graph && subject[:subject]['@id'] == subject_to_embed['@id']
279
373
  end
280
374
  end
281
375
 
@@ -308,7 +402,7 @@ module JSON::LD
308
402
  # @param id the @id of the embed to remove.
309
403
  def remove_embed(state, id)
310
404
  # get existing embed
311
- embeds = state[:uniqueEmbeds];
405
+ embeds = state[:uniqueEmbeds][state[:graph]];
312
406
  embed = embeds[id];
313
407
  property = embed[:property];
314
408
 
@@ -368,7 +462,31 @@ module JSON::LD
368
462
  # @param [Hash] flags the current framing flags.
369
463
  # @return [Array<Hash>] the implicit frame.
370
464
  def create_implicit_frame(flags)
371
- [flags.keys.inject({}) {|memo, key| memo["@#{key}"] = [flags[key]]; memo}]
465
+ flags.keys.inject({}) {|memo, key| memo["@#{key}"] = [flags[key]]; memo}
466
+ end
467
+
468
+ private
469
+ # Node matches if it is a node, and matches the pattern as a frame
470
+ def node_match?(pattern, value, state, flags)
471
+ return false unless value['@id']
472
+ node_object = state[:subjects][value['@id']]
473
+ node_object && filter_subject(node_object, pattern, state, flags)
474
+ end
475
+
476
+ # Value matches if it is a value, and matches the value pattern.
477
+ #
478
+ # * `pattern` is empty
479
+ # * @values are the same, or `pattern[@value]` is a wildcard, and
480
+ # * @types are the same or `value[@type]` is not null and `pattern[@type]` is `{}`, or `value[@type]` is null and `pattern[@type]` is null or `[]`, and
481
+ # * @languages are the same or `value[@language]` is not null and `pattern[@language]` is `{}`, or `value[@language]` is null and `pattern[@language]` is null or `[]`.
482
+ def value_match?(pattern, value)
483
+ v1, t1, l1 = value['@value'], value['@type'], value['@language']
484
+ v2, t2, l2 = Array(pattern['@value']), Array(pattern['@type']), Array(pattern['@language'])
485
+ return true if (v2 + t2 + l2).empty?
486
+ return false unless v2.include?(v1) || v2 == [{}]
487
+ return false unless t2.include?(t1) || t1 && t2 == [{}] || t1.nil? && (t2 || []) == []
488
+ return false unless l2.include?(l1) || l1 && l2 == [{}] || l1.nil? && (l2 || []) == []
489
+ true
372
490
  end
373
491
  end
374
492
  end
@@ -16,7 +16,7 @@ module JSON::LD
16
16
  @context = case @options[:context]
17
17
  when nil then nil
18
18
  when Context then @options[:context]
19
- else Context.new.parse(@options[:context])
19
+ else Context.parse(@options[:context])
20
20
  end
21
21
 
22
22
  #log_debug("prologue") {"context: #{context.inspect}"}