json-ld 2.2.1 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +55 -55
- data/VERSION +1 -1
- data/lib/json/ld.rb +4 -2
- data/lib/json/ld/api.rb +49 -59
- data/lib/json/ld/compact.rb +60 -56
- data/lib/json/ld/context.rb +52 -40
- data/lib/json/ld/expand.rb +53 -61
- data/lib/json/ld/extensions.rb +31 -16
- data/lib/json/ld/flatten.rb +99 -90
- data/lib/json/ld/format.rb +2 -2
- data/lib/json/ld/frame.rb +47 -30
- data/lib/json/ld/from_rdf.rb +31 -23
- data/lib/json/ld/resource.rb +1 -1
- data/lib/json/ld/to_rdf.rb +4 -2
- data/lib/json/ld/utils.rb +25 -35
- data/lib/json/ld/writer.rb +25 -1
- data/spec/api_spec.rb +1 -0
- data/spec/compact_spec.rb +536 -31
- data/spec/context_spec.rb +109 -43
- data/spec/expand_spec.rb +413 -18
- data/spec/flatten_spec.rb +107 -27
- data/spec/frame_spec.rb +255 -34
- data/spec/from_rdf_spec.rb +102 -3
- data/spec/streaming_writer_spec.rb +8 -9
- data/spec/suite_compact_spec.rb +2 -2
- data/spec/suite_expand_spec.rb +2 -2
- data/spec/suite_flatten_spec.rb +2 -2
- data/spec/suite_frame_spec.rb +2 -2
- data/spec/suite_from_rdf_spec.rb +2 -3
- data/spec/suite_helper.rb +57 -61
- data/spec/suite_remote_doc_spec.rb +2 -2
- data/spec/suite_to_rdf_spec.rb +4 -4
- data/spec/to_rdf_spec.rb +88 -1
- data/spec/writer_spec.rb +5 -6
- metadata +5 -7
- data/spec/suite_error_spec.rb +0 -16
data/lib/json/ld/extensions.rb
CHANGED
@@ -7,27 +7,42 @@ module RDF
|
|
7
7
|
Node.new(id + value.to_s)
|
8
8
|
end
|
9
9
|
end
|
10
|
-
end
|
11
10
|
|
12
|
-
class
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
memo[kw] = "@#{KW_ORDER.index(kw)}"
|
22
|
-
end.freeze
|
11
|
+
class Statement
|
12
|
+
# Validate extended RDF
|
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)
|
18
|
+
end
|
19
|
+
end
|
23
20
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
21
|
+
class URI
|
22
|
+
# Validate extended RDF
|
23
|
+
def valid_extended?
|
24
|
+
self.valid?
|
28
25
|
end
|
29
26
|
end
|
30
27
|
|
28
|
+
class Node
|
29
|
+
# Validate extended RDF
|
30
|
+
def valid_extended?
|
31
|
+
self.valid?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Literal
|
36
|
+
# Validate extended RDF
|
37
|
+
def valid_extended?
|
38
|
+
return false if language? && language.to_s !~ /^[a-zA-Z]+(-[a-zA-Z0-9]+)*$/
|
39
|
+
return false if datatype? && datatype.invalid?
|
40
|
+
value.is_a?(String)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Array
|
31
46
|
# Order terms, length first, then lexographically
|
32
47
|
def term_sort
|
33
48
|
self.sort do |a, b|
|
data/lib/json/ld/flatten.rb
CHANGED
@@ -7,111 +7,120 @@ module JSON::LD
|
|
7
7
|
##
|
8
8
|
# 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
9
|
#
|
10
|
-
# @param [Array, Hash]
|
10
|
+
# @param [Array, Hash] element
|
11
11
|
# Expanded JSON-LD input
|
12
|
-
# @param [Hash]
|
13
|
-
# @param [String]
|
12
|
+
# @param [Hash] graph_map A map of graph name to subjects
|
13
|
+
# @param [String] active_graph
|
14
14
|
# The name of the currently active graph that the processor should use when processing.
|
15
|
-
# @param [String]
|
16
|
-
#
|
17
|
-
# @param [
|
18
|
-
#
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
15
|
+
# @param [String] active_subject (nil)
|
16
|
+
# Node identifier
|
17
|
+
# @param [String] active_property (nil)
|
18
|
+
# Property within current node
|
19
|
+
# @param [Array] list (nil)
|
20
|
+
# Used when property value is a list
|
21
|
+
def create_node_map(element, graph_map,
|
22
|
+
active_graph: '@default',
|
23
|
+
active_subject: nil,
|
24
|
+
active_property: nil,
|
25
|
+
list: nil)
|
26
|
+
log_debug("node_map") {"active_graph: #{active_graph}, element: #{element.inspect}, active_subject: #{active_subject}"}
|
27
|
+
if element.is_a?(Array)
|
28
|
+
# 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.
|
29
|
+
element.map do |o|
|
30
|
+
create_node_map(o, graph_map,
|
31
|
+
active_graph: active_graph,
|
32
|
+
active_subject: active_subject,
|
33
|
+
active_property: active_property,
|
34
|
+
list: list)
|
35
|
+
end
|
36
|
+
elsif !element.is_a?(Hash)
|
37
|
+
raise "Expected hash or array to create_node_map, got #{element.inspect}"
|
38
|
+
else
|
39
|
+
graph = (graph_map[active_graph] ||= {})
|
40
|
+
subject_node = graph[active_subject]
|
33
41
|
|
34
|
-
|
35
|
-
|
42
|
+
# Transform BNode types
|
43
|
+
if element.has_key?('@type')
|
44
|
+
element['@type'] = Array(element['@type']).map {|t| blank_node?(t) ? namer.get_name(t) : t}
|
45
|
+
end
|
36
46
|
|
37
|
-
|
38
|
-
if
|
39
|
-
|
40
|
-
|
47
|
+
if value?(element)
|
48
|
+
element['@type'] = element['@type'].first if element ['@type']
|
49
|
+
if list.nil?
|
50
|
+
add_value(subject_node, active_property, element, property_is_array: true, allow_duplicate: false)
|
51
|
+
else
|
52
|
+
list['@list'] << element
|
41
53
|
end
|
54
|
+
elsif list?(element)
|
55
|
+
result = {'@list' => []}
|
56
|
+
create_node_map(element['@list'], graph_map,
|
57
|
+
active_graph: active_graph,
|
58
|
+
active_subject: active_subject,
|
59
|
+
active_property: active_property,
|
60
|
+
list: result)
|
61
|
+
if list.nil?
|
62
|
+
add_value(subject_node, active_property, result, property_is_array: true)
|
63
|
+
else
|
64
|
+
list['@list'] << result
|
65
|
+
end
|
66
|
+
else
|
67
|
+
# Element is a node object
|
68
|
+
id = element.delete('@id')
|
69
|
+
id = namer.get_name(id) if blank_node?(id)
|
42
70
|
|
43
|
-
|
44
|
-
list << {'@id' => name} if list
|
45
|
-
|
46
|
-
# create new subject or merge into existing one
|
47
|
-
subject = (graphs[graph] ||= {})[name] ||= {'@id' => name}
|
71
|
+
node = graph[id] ||= {'@id' => id}
|
48
72
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
referenced_node, reverse_map = {'@id' => name}, objects
|
57
|
-
reverse_map.each do |reverse_property, items|
|
58
|
-
items.each do |item|
|
59
|
-
item_name = item['@id']
|
60
|
-
item_name = namer.get_name(item_name) if blank_node?(item_name)
|
61
|
-
create_node_map(item, graphs, graph: graph, name: item_name)
|
62
|
-
add_value(graphs[graph][item_name],
|
63
|
-
reverse_property,
|
64
|
-
referenced_node,
|
65
|
-
property_is_array: true,
|
66
|
-
allow_duplicate: false)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
when '@graph'
|
70
|
-
graphs[name] ||= {}
|
71
|
-
create_node_map(objects, graphs, graph: name)
|
72
|
-
when /^@(?!type)/
|
73
|
-
# copy non-@type keywords
|
74
|
-
if property == '@index' && subject['@index']
|
75
|
-
raise JsonLdError::ConflictingIndexes,
|
76
|
-
"Element already has index #{subject['@index']} dfferent from #{input['@index']}" if
|
77
|
-
subject['@index'] != input['@index']
|
78
|
-
subject['@index'] = input.delete('@index')
|
79
|
-
end
|
80
|
-
subject[property] = objects
|
73
|
+
if active_subject.is_a?(Hash)
|
74
|
+
# If subject is a hash, then we're processing a reverse-property relationship.
|
75
|
+
add_value(node, active_property, active_subject, property_is_array: true, allow_duplicate: false)
|
76
|
+
elsif active_property
|
77
|
+
reference = {'@id' => id}
|
78
|
+
if list.nil?
|
79
|
+
add_value(subject_node, active_property, reference, property_is_array: true, allow_duplicate: false)
|
81
80
|
else
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
add_value(subject, property, [], property_is_array: true) if objects.empty?
|
81
|
+
list['@list'] << reference
|
82
|
+
end
|
83
|
+
end
|
86
84
|
|
87
|
-
|
88
|
-
|
85
|
+
if element.has_key?('@type')
|
86
|
+
add_value(node, '@type', element.delete('@type'), property_is_array: true, allow_duplicate: false)
|
87
|
+
end
|
89
88
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
89
|
+
if element['@index']
|
90
|
+
raise JsonLdError::ConflictingIndexes,
|
91
|
+
"Element already has index #{node['@index']} dfferent from #{element['@index']}" if
|
92
|
+
node.key?('@index') && node['@index'] != element['@index']
|
93
|
+
node['@index'] = element.delete('@index')
|
94
|
+
end
|
94
95
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
else
|
104
|
-
# handle @value
|
105
|
-
create_node_map(o, graphs, graph: graph, name: name)
|
106
|
-
add_value(subject, property, o, property_is_array: true, allow_duplicate: false)
|
107
|
-
end
|
96
|
+
if element['@reverse']
|
97
|
+
referenced_node, reverse_map = {'@id' => id}, element.delete('@reverse')
|
98
|
+
reverse_map.each do |property, values|
|
99
|
+
values.each do |value|
|
100
|
+
create_node_map(value, graph_map,
|
101
|
+
active_graph: active_graph,
|
102
|
+
active_subject: referenced_node,
|
103
|
+
active_property: property)
|
108
104
|
end
|
109
105
|
end
|
110
106
|
end
|
107
|
+
|
108
|
+
if element['@graph']
|
109
|
+
create_node_map(element.delete('@graph'), graph_map,
|
110
|
+
active_graph: id)
|
111
|
+
end
|
112
|
+
|
113
|
+
element.keys.sort.each do |property|
|
114
|
+
value = element[property]
|
115
|
+
|
116
|
+
property = namer.get_name(property) if blank_node?(property)
|
117
|
+
node[property] ||= []
|
118
|
+
create_node_map(value, graph_map,
|
119
|
+
active_graph: active_graph,
|
120
|
+
active_subject: id,
|
121
|
+
active_property: property)
|
122
|
+
end
|
111
123
|
end
|
112
|
-
else
|
113
|
-
# add non-object to list
|
114
|
-
list << input if list
|
115
124
|
end
|
116
125
|
end
|
117
126
|
|
data/lib/json/ld/format.rb
CHANGED
@@ -63,12 +63,12 @@ module JSON::LD
|
|
63
63
|
# If files are empty, either use options[:execute]
|
64
64
|
input = options[:evaluate] ? StringIO.new(options[:evaluate]) : STDIN
|
65
65
|
input.set_encoding(options.fetch(:encoding, Encoding::UTF_8))
|
66
|
-
JSON::LD::API.expand(input,
|
66
|
+
JSON::LD::API.expand(input, validate: false, **options) do |expanded|
|
67
67
|
out.puts expanded.to_json(JSON::LD::JSON_STATE)
|
68
68
|
end
|
69
69
|
else
|
70
70
|
files.each do |file|
|
71
|
-
JSON::LD::API.expand(file,
|
71
|
+
JSON::LD::API.expand(file, validate: false, **options) do |expanded|
|
72
72
|
out.puts expanded.to_json(JSON::LD::JSON_STATE)
|
73
73
|
end
|
74
74
|
end
|
data/lib/json/ld/frame.rb
CHANGED
@@ -20,13 +20,12 @@ module JSON::LD
|
|
20
20
|
# @option options [String] :property (nil)
|
21
21
|
# The parent property.
|
22
22
|
# @raise [JSON::LD::InvalidFrame]
|
23
|
-
def frame(state, subjects, frame, **options)
|
24
|
-
log_depth do
|
25
|
-
log_debug("frame") {"subjects: #{subjects.inspect}"}
|
26
|
-
log_debug("frame") {"frame: #{frame.to_json(JSON_STATE)}"}
|
27
|
-
log_debug("frame") {"property: #{
|
23
|
+
def frame(state, subjects, frame, parent: nil, property: nil, **options)
|
24
|
+
#log_depth do
|
25
|
+
#log_debug("frame") {"subjects: #{subjects.inspect}"}
|
26
|
+
#log_debug("frame") {"frame: #{frame.to_json(JSON_STATE)}"}
|
27
|
+
#log_debug("frame") {"property: #{property.inspect}"}
|
28
28
|
|
29
|
-
parent, property = options[:parent], options[:property]
|
30
29
|
# Validate the frame
|
31
30
|
validate_frame(frame)
|
32
31
|
frame = frame.first if frame.is_a?(Array)
|
@@ -46,10 +45,10 @@ module JSON::LD
|
|
46
45
|
matches = filter_subjects(state, subjects, frame, flags)
|
47
46
|
|
48
47
|
# For each id and node from the set of matched subjects ordered by id
|
49
|
-
matches.keys.
|
48
|
+
matches.keys.sort.each do |id|
|
50
49
|
subject = matches[id]
|
51
50
|
|
52
|
-
# Note: In order to treat each top-level match as a compartmentalized result, clear the unique embedded subjects map when the property is
|
51
|
+
# Note: In order to treat each top-level match as a compartmentalized result, clear the unique embedded subjects map when the property is nil, which only occurs at the top-level.
|
53
52
|
if property.nil?
|
54
53
|
state[:uniqueEmbeds] = {state[:graph] => {}}
|
55
54
|
else
|
@@ -105,13 +104,13 @@ module JSON::LD
|
|
105
104
|
if recurse
|
106
105
|
state[:graphStack].push(state[:graph])
|
107
106
|
state[:graph] = id
|
108
|
-
frame(state, state[:graphMap][id].keys, [subframe],
|
107
|
+
frame(state, state[:graphMap][id].keys, [subframe], parent: output, property: '@graph', **options)
|
109
108
|
state[:graph] = state[:graphStack].pop
|
110
109
|
end
|
111
110
|
end
|
112
111
|
|
113
112
|
# iterate over subject properties in order
|
114
|
-
subject.keys.
|
113
|
+
subject.keys.sort.each do |prop|
|
115
114
|
objects = subject[prop]
|
116
115
|
|
117
116
|
# copy keywords to output
|
@@ -138,14 +137,14 @@ module JSON::LD
|
|
138
137
|
src = o['@list']
|
139
138
|
src.each do |oo|
|
140
139
|
if node_reference?(oo)
|
141
|
-
frame(state, [oo['@id']], subframe,
|
140
|
+
frame(state, [oo['@id']], subframe, parent: list, property: '@list', **options)
|
142
141
|
else
|
143
142
|
add_frame_output(list, '@list', oo.dup)
|
144
143
|
end
|
145
144
|
end
|
146
145
|
when node_reference?(o)
|
147
146
|
# recurse into subject reference
|
148
|
-
frame(state, [o['@id']], subframe,
|
147
|
+
frame(state, [o['@id']], subframe, parent: output, property: prop, **options)
|
149
148
|
when value_match?(subframe, o)
|
150
149
|
# Include values if they match
|
151
150
|
add_frame_output(output, prop, o.dup)
|
@@ -154,15 +153,14 @@ module JSON::LD
|
|
154
153
|
end
|
155
154
|
|
156
155
|
# handle defaults in order
|
157
|
-
frame.keys.
|
156
|
+
frame.keys.sort.each do |prop|
|
158
157
|
next if prop.start_with?('@')
|
159
158
|
|
160
159
|
# if omit default is off, then include default values for properties that appear in the next frame but are not in the matching subject
|
161
160
|
n = frame[prop].first || {}
|
162
161
|
omit_default_on = get_frame_flag(n, options, :omitDefault)
|
163
162
|
if !omit_default_on && !output[prop]
|
164
|
-
preserve = n.fetch('@default', '@null').dup
|
165
|
-
preserve = [preserve] unless preserve.is_a?(Array)
|
163
|
+
preserve = as_array(n.fetch('@default', '@null').dup)
|
166
164
|
output[prop] = [{'@preserve' => preserve}]
|
167
165
|
end
|
168
166
|
end
|
@@ -174,7 +172,7 @@ module JSON::LD
|
|
174
172
|
# Node has property referencing this subject
|
175
173
|
# recurse into reference
|
176
174
|
(output['@reverse'] ||= {})[reverse_prop] ||= []
|
177
|
-
frame(state, [r_id], subframe,
|
175
|
+
frame(state, [r_id], subframe, parent: output['@reverse'][reverse_prop], property: property, **options)
|
178
176
|
end
|
179
177
|
end
|
180
178
|
end
|
@@ -185,7 +183,7 @@ module JSON::LD
|
|
185
183
|
# pop matching subject from circular ref-checking stack
|
186
184
|
state[:subjectStack].pop()
|
187
185
|
end
|
188
|
-
end
|
186
|
+
#end
|
189
187
|
end
|
190
188
|
|
191
189
|
##
|
@@ -213,19 +211,45 @@ module JSON::LD
|
|
213
211
|
end
|
214
212
|
end
|
215
213
|
|
214
|
+
##
|
215
|
+
# Prune BNode identifiers recursively
|
216
|
+
#
|
217
|
+
# @param [Array, Hash] input
|
218
|
+
# @param [Array<String>] bnodes_to_clear
|
219
|
+
# @return [Array, Hash]
|
220
|
+
def prune_bnodes(input, bnodes_to_clear)
|
221
|
+
result = case input
|
222
|
+
when Array
|
223
|
+
# If, after replacement, an array contains only the value null remove the value, leaving an empty array.
|
224
|
+
input.map {|o| prune_bnodes(o, bnodes_to_clear)}.compact
|
225
|
+
when Hash
|
226
|
+
output = Hash.new
|
227
|
+
input.each do |key, value|
|
228
|
+
if context.expand_iri(key) == '@id' && bnodes_to_clear.include?(value)
|
229
|
+
# Don't add this to output, as it is pruned as being superfluous
|
230
|
+
else
|
231
|
+
output[key] = prune_bnodes(value, bnodes_to_clear)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
output
|
235
|
+
else
|
236
|
+
input
|
237
|
+
end
|
238
|
+
result
|
239
|
+
end
|
240
|
+
|
216
241
|
##
|
217
242
|
# Replace @preserve keys with the values, also replace @null with null.
|
218
243
|
#
|
219
244
|
# Optionally, remove BNode identifiers only used once.
|
220
245
|
#
|
221
246
|
# @param [Array, Hash] input
|
222
|
-
# @param [Array<String>] bnodes_to_clear
|
223
247
|
# @return [Array, Hash]
|
224
|
-
def cleanup_preserve(input
|
248
|
+
def cleanup_preserve(input)
|
225
249
|
result = case input
|
226
250
|
when Array
|
227
251
|
# If, after replacement, an array contains only the value null remove the value, leaving an empty array.
|
228
|
-
v = input.map {|o| cleanup_preserve(o
|
252
|
+
v = input.map {|o| cleanup_preserve(o)}.compact
|
229
253
|
|
230
254
|
# If the array contains a single member, which is itself an array, use that value as the result
|
231
255
|
(v.length == 1 && v.first.is_a?(Array)) ? v.first : v
|
@@ -234,11 +258,9 @@ module JSON::LD
|
|
234
258
|
input.each do |key, value|
|
235
259
|
if key == '@preserve'
|
236
260
|
# replace all key-value pairs where the key is @preserve with the value from the key-pair
|
237
|
-
output = cleanup_preserve(value
|
238
|
-
elsif context.expand_iri(key) == '@id' && bnodes_to_clear.include?(value)
|
239
|
-
# Don't add this to output, as it is pruned as being superfluous
|
261
|
+
output = cleanup_preserve(value)
|
240
262
|
else
|
241
|
-
v = cleanup_preserve(value
|
263
|
+
v = cleanup_preserve(value)
|
242
264
|
|
243
265
|
# Because we may have added a null value to an array, we need to clean that up, if we possible
|
244
266
|
v = v.first if v.is_a?(Array) && v.length == 1 && !context.as_array?(key)
|
@@ -275,8 +297,6 @@ module JSON::LD
|
|
275
297
|
end
|
276
298
|
end
|
277
299
|
|
278
|
-
EXCLUDED_FRAMING_KEYWORDS = Set.new(%w(@default @embed @explicit @omitDefault @requireAll)).freeze
|
279
|
-
|
280
300
|
##
|
281
301
|
# Returns true if the given node matches the given frame.
|
282
302
|
#
|
@@ -319,7 +339,6 @@ module JSON::LD
|
|
319
339
|
else
|
320
340
|
# Match on specific @type
|
321
341
|
return !(v & node_values).empty?
|
322
|
-
false
|
323
342
|
end
|
324
343
|
when /@/
|
325
344
|
# Skip other keywords
|
@@ -329,8 +348,6 @@ module JSON::LD
|
|
329
348
|
if v = v.first
|
330
349
|
validate_frame(v)
|
331
350
|
has_default = v.has_key?('@default')
|
332
|
-
# Exclude framing keywords
|
333
|
-
v = v.reject {|kk,vv| EXCLUDED_FRAMING_KEYWORDS.include?(kk)}
|
334
351
|
end
|
335
352
|
|
336
353
|
|
@@ -348,7 +365,7 @@ module JSON::LD
|
|
348
365
|
# node does not match if values is not empty and the value of property in frame is match none.
|
349
366
|
return false unless node_values.empty?
|
350
367
|
true
|
351
|
-
when
|
368
|
+
when Hash # Empty other than framing keywords
|
352
369
|
# node matches if values is not empty and the value of property in frame is wildcard
|
353
370
|
!node_values.empty?
|
354
371
|
else
|