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.
- 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}"}
|