json-ld 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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}"}