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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/lib/json/ld.rb +1 -1
- data/lib/json/ld/api.rb +49 -44
- data/lib/json/ld/compact.rb +4 -4
- data/lib/json/ld/context.rb +20 -8
- data/lib/json/ld/expand.rb +73 -26
- data/lib/json/ld/flatten.rb +31 -2
- data/lib/json/ld/format.rb +4 -4
- data/lib/json/ld/frame.rb +182 -64
- data/lib/json/ld/streaming_writer.rb +1 -1
- data/lib/json/ld/to_rdf.rb +1 -1
- data/lib/json/ld/utils.rb +10 -1
- data/spec/context_spec.rb +20 -7
- data/spec/expand_spec.rb +19 -0
- data/spec/format_spec.rb +6 -6
- data/spec/frame_spec.rb +1250 -501
- data/spec/suite_helper.rb +1 -1
- metadata +27 -27
- data/spec/test-files/test-1-automatic.json +0 -10
- data/spec/test-files/test-2-automatic.json +0 -27
- data/spec/test-files/test-4-automatic.json +0 -10
- data/spec/test-files/test-5-automatic.json +0 -13
- data/spec/test-files/test-6-automatic.json +0 -10
- data/spec/test-files/test-7-automatic.json +0 -20
- data/spec/test-files/test-8-automatic.json +0 -1
data/lib/json/ld/flatten.rb
CHANGED
@@ -68,8 +68,7 @@ module JSON::LD
|
|
68
68
|
end
|
69
69
|
when '@graph'
|
70
70
|
graphs[name] ||= {}
|
71
|
-
|
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
|
data/lib/json/ld/format.rb
CHANGED
@@ -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: "
|
155
|
+
description: "Frame JSON-LD or parsed RDF",
|
156
156
|
parse: false,
|
157
|
-
help: "
|
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
|
data/lib/json/ld/frame.rb
CHANGED
@@ -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(
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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,
|
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
|
-
|
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
|
-
|
114
|
-
#
|
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 [
|
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[:
|
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
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
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
|
-
|
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
|
-
|
254
|
-
|
255
|
-
|
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
|
-
#
|
260
|
-
|
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(
|
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
|
-
|
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.
|
19
|
+
else Context.parse(@options[:context])
|
20
20
|
end
|
21
21
|
|
22
22
|
#log_debug("prologue") {"context: #{context.inspect}"}
|