json-ld 3.1.3 → 3.1.4
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/README.md +7 -1
- data/VERSION +1 -1
- data/lib/json/ld.rb +3 -0
- data/lib/json/ld/api.rb +31 -24
- data/lib/json/ld/compact.rb +37 -21
- data/lib/json/ld/context.rb +532 -473
- data/lib/json/ld/expand.rb +131 -68
- data/lib/json/ld/from_rdf.rb +7 -8
- data/lib/json/ld/reader.rb +19 -10
- data/lib/json/ld/streaming_reader.rb +578 -0
- data/spec/context_spec.rb +4 -42
- data/spec/reader_spec.rb +33 -34
- data/spec/streaming_reader_spec.rb +237 -0
- data/spec/suite_helper.rb +14 -8
- metadata +6 -3
data/lib/json/ld/from_rdf.rb
CHANGED
@@ -14,10 +14,9 @@ module JSON::LD
|
|
14
14
|
# @param [Boolean] useRdfType (false)
|
15
15
|
# If set to `true`, the JSON-LD processor will treat `rdf:type` like a normal property instead of using `@type`.
|
16
16
|
# @param [Boolean] useNativeTypes (false) use native representations
|
17
|
-
#
|
18
|
-
# Ensure output objects have keys ordered properly
|
17
|
+
#
|
19
18
|
# @return [Array<Hash>] the JSON-LD document in normalized form
|
20
|
-
def from_statements(dataset, useRdfType: false, useNativeTypes: false
|
19
|
+
def from_statements(dataset, useRdfType: false, useNativeTypes: false)
|
21
20
|
default_graph = {}
|
22
21
|
graph_map = {'@default' => default_graph}
|
23
22
|
referenced_once = {}
|
@@ -34,7 +33,7 @@ module JSON::LD
|
|
34
33
|
dataset.each do |statement|
|
35
34
|
#log_debug("statement") { statement.to_nquads.chomp}
|
36
35
|
|
37
|
-
name = statement.graph_name ? ec.expand_iri(statement.graph_name).to_s : '@default'
|
36
|
+
name = statement.graph_name ? ec.expand_iri(statement.graph_name, base: @options[:base]).to_s : '@default'
|
38
37
|
|
39
38
|
# Create a graph entry as needed
|
40
39
|
node_map = graph_map[name] ||= {}
|
@@ -42,7 +41,7 @@ module JSON::LD
|
|
42
41
|
|
43
42
|
default_graph[name] ||= {'@id' => name} unless name == '@default'
|
44
43
|
|
45
|
-
subject = ec.expand_iri(statement.subject, as_string: true)
|
44
|
+
subject = ec.expand_iri(statement.subject, as_string: true, base: @options[:base])
|
46
45
|
node = node_map[subject] ||= {'@id' => subject}
|
47
46
|
|
48
47
|
# If predicate is rdf:datatype, note subject in compound literal subjects map
|
@@ -65,7 +64,7 @@ module JSON::LD
|
|
65
64
|
statement.object,
|
66
65
|
rdfDirection: @options[:rdfDirection],
|
67
66
|
useNativeTypes: useNativeTypes,
|
68
|
-
|
67
|
+
base: @options[:base])
|
69
68
|
|
70
69
|
merge_value(node, statement.predicate.to_s, value)
|
71
70
|
|
@@ -147,11 +146,11 @@ module JSON::LD
|
|
147
146
|
end
|
148
147
|
|
149
148
|
result = []
|
150
|
-
default_graph.keys.opt_sort(ordered: ordered).each do |subject|
|
149
|
+
default_graph.keys.opt_sort(ordered: @options[:ordered]).each do |subject|
|
151
150
|
node = default_graph[subject]
|
152
151
|
if graph_map.has_key?(subject)
|
153
152
|
node['@graph'] = []
|
154
|
-
graph_map[subject].keys.opt_sort(ordered: ordered).each do |s|
|
153
|
+
graph_map[subject].keys.opt_sort(ordered: @options[:ordered]).each do |s|
|
155
154
|
n = graph_map[subject][s]
|
156
155
|
n.delete(:usages)
|
157
156
|
node['@graph'] << n unless node_reference?(n)
|
data/lib/json/ld/reader.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
|
2
|
+
|
3
3
|
module JSON::LD
|
4
4
|
##
|
5
5
|
# A JSON-LD parser in Ruby.
|
@@ -7,6 +7,7 @@ module JSON::LD
|
|
7
7
|
# @see https://www.w3.org/TR/json-ld11-api
|
8
8
|
# @author [Gregg Kellogg](http://greggkellogg.net/)
|
9
9
|
class Reader < RDF::Reader
|
10
|
+
include StreamingReader
|
10
11
|
format Format
|
11
12
|
|
12
13
|
##
|
@@ -46,6 +47,12 @@ module JSON::LD
|
|
46
47
|
control: :select,
|
47
48
|
on: ["--rdf-direction DIR", %w(i18n-datatype compound-literal)],
|
48
49
|
description: "How to serialize literal direction (i18n-datatype compound-literal)") {|arg| RDF::URI(arg)},
|
50
|
+
RDF::CLI::Option.new(
|
51
|
+
symbol: :stream,
|
52
|
+
datatype: TrueClass,
|
53
|
+
control: :checkbox,
|
54
|
+
on: ["--[no-]stream"],
|
55
|
+
description: "Optimize for streaming JSON-LD to RDF.") {|arg| arg},
|
49
56
|
]
|
50
57
|
end
|
51
58
|
|
@@ -63,13 +70,11 @@ module JSON::LD
|
|
63
70
|
options[:base_uri] ||= options[:base]
|
64
71
|
super do
|
65
72
|
@options[:base] ||= base_uri.to_s if base_uri
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
StringIO.new(input.to_s.sub(%r(\A[^{\[]*)m, '').sub(%r([^}\]]*\Z)m, ''))
|
72
|
-
end
|
73
|
+
# Trim non-JSON stuff in script.
|
74
|
+
@doc = if input.respond_to?(:read)
|
75
|
+
input
|
76
|
+
else
|
77
|
+
StringIO.new(input.to_s.sub(%r(\A[^{\[]*)m, '').sub(%r([^}\]]*\Z)m, ''))
|
73
78
|
end
|
74
79
|
|
75
80
|
if block_given?
|
@@ -85,7 +90,11 @@ module JSON::LD
|
|
85
90
|
# @private
|
86
91
|
# @see RDF::Reader#each_statement
|
87
92
|
def each_statement(&block)
|
88
|
-
|
93
|
+
if @options[:stream]
|
94
|
+
stream_statement(&block)
|
95
|
+
else
|
96
|
+
API.toRdf(@doc, **@options, &block)
|
97
|
+
end
|
89
98
|
rescue ::JSON::ParserError, ::JSON::LD::JsonLdError => e
|
90
99
|
log_fatal("Failed to parse input document: #{e.message}", exception: RDF::ReaderError)
|
91
100
|
end
|
@@ -95,7 +104,7 @@ module JSON::LD
|
|
95
104
|
# @see RDF::Reader#each_triple
|
96
105
|
def each_triple(&block)
|
97
106
|
if block_given?
|
98
|
-
|
107
|
+
each_statement do |statement|
|
99
108
|
yield(*statement.to_triple)
|
100
109
|
end
|
101
110
|
end
|
@@ -0,0 +1,578 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require 'json/ld'
|
3
|
+
require 'json/ld/expand'
|
4
|
+
require 'json/ld/to_rdf'
|
5
|
+
|
6
|
+
module JSON::LD
|
7
|
+
##
|
8
|
+
# A streaming JSON-LD parser in Ruby.
|
9
|
+
#
|
10
|
+
# @see http://json-ld.org/spec/ED/20110507/
|
11
|
+
# @author [Gregg Kellogg](http://greggkellogg.net/)
|
12
|
+
module StreamingReader
|
13
|
+
include Utils
|
14
|
+
include JSON::LD::ToRDF # For value object conversion
|
15
|
+
|
16
|
+
# The base URI to use when resolving relative URIs
|
17
|
+
# @return [RDF::URI]
|
18
|
+
attr_reader :base
|
19
|
+
attr_reader :namer
|
20
|
+
|
21
|
+
def self.format; JSON::LD::Format; end
|
22
|
+
|
23
|
+
##
|
24
|
+
# @see RDF::Reader#each_statement
|
25
|
+
def stream_statement(&block)
|
26
|
+
unique_bnodes, rename_bnodes = @options[:unique_bnodes], @options.fetch(:rename_bnodes, true)
|
27
|
+
# FIXME: document loader doesn't stream
|
28
|
+
@base = RDF::URI(@options[:base] || base_uri)
|
29
|
+
value = MultiJson.load(@doc, **@options)
|
30
|
+
context_ref = @options[:expandContext]
|
31
|
+
#context_ref = @options.fetch(:expandContext, remote_doc.contextUrl)
|
32
|
+
context = Context.parse(context_ref, **@options)
|
33
|
+
|
34
|
+
@namer = unique_bnodes ? BlankNodeUniqer.new : (rename_bnodes ? BlankNodeNamer.new("b") : BlankNodeMapper.new)
|
35
|
+
# Namer for naming provisional nodes, which may be determined later to be actual
|
36
|
+
@provisional_namer = BlankNodeNamer.new("p")
|
37
|
+
|
38
|
+
parse_object(value, nil, context, graph_is_named: false) do |st|
|
39
|
+
# Only output reasonably valid triples
|
40
|
+
if st.to_a.all? {|r| r.is_a?(RDF::Term) && (r.uri? ? r.valid? : true)}
|
41
|
+
block.call(st)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
rescue ::JSON::ParserError, ::JSON::LD::JsonLdError => e
|
45
|
+
log_fatal("Failed to parse input document: #{e.message}", exception: RDF::ReaderError)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# Parse a node object, or array of node objects
|
51
|
+
#
|
52
|
+
# @param [Array, Hash] input
|
53
|
+
# @param [String] active_property
|
54
|
+
# The unexpanded property referencing this object
|
55
|
+
# @param [Context] context
|
56
|
+
# @param [RDF::Resource] subject referencing this object
|
57
|
+
# @param [RDF::URI] predicate the predicate part of the reference
|
58
|
+
# @param [Boolean] from_map
|
59
|
+
# Expanding from a map, which could be an `@type` map, so don't clear out context term definitions
|
60
|
+
# @param [Boolean] graph_is_named
|
61
|
+
# Use of `@graph` implies a named graph; not true at the top-level.
|
62
|
+
# @param [RDF::URI] extra_type from a type map
|
63
|
+
# @param [String] language from a language map
|
64
|
+
# @param [RDF::Resource] node_id from an id map
|
65
|
+
# @return [void]
|
66
|
+
def parse_object(input, active_property, context,
|
67
|
+
subject: nil, predicate: nil, from_map: false,
|
68
|
+
extra_type: nil, language: nil, node_id: nil,
|
69
|
+
graph_is_named: true, &block)
|
70
|
+
|
71
|
+
# Skip predicates that look like a BNode
|
72
|
+
if predicate.to_s.start_with?('_:')
|
73
|
+
warn "[DEPRECATION] Blank Node properties deprecated in JSON-LD 1.1."
|
74
|
+
return
|
75
|
+
end
|
76
|
+
|
77
|
+
if input.is_a?(Array)
|
78
|
+
input.each {|e| parse_object(e, active_property, context, subject: subject, predicate: predicate, from_map: from_map, &block)}
|
79
|
+
return
|
80
|
+
end
|
81
|
+
|
82
|
+
# Note that we haven't parsed an @id key, so have no subject
|
83
|
+
have_id, node_reference, is_list_or_set = false, false, false
|
84
|
+
node_id ||= RDF::Node.new(@provisional_namer.get_sym)
|
85
|
+
# For keeping statements not yet ready to be emitted
|
86
|
+
provisional_statements = []
|
87
|
+
value_object = {}
|
88
|
+
|
89
|
+
# Use a term-specific context, if defined, based on the non-type-scoped context.
|
90
|
+
property_scoped_context = context.term_definitions[active_property].context if active_property && context.term_definitions[active_property]
|
91
|
+
|
92
|
+
# Revert any previously type-scoped term definitions, unless this is from a map, a value object or a subject reference
|
93
|
+
# FIXME
|
94
|
+
if input.is_a?(Hash) && context.previous_context
|
95
|
+
expanded_key_map = input.keys.inject({}) do |memo, key|
|
96
|
+
memo.merge(key => context.expand_iri(key, vocab: true, as_string: true, base: base))
|
97
|
+
end
|
98
|
+
revert_context = !from_map &&
|
99
|
+
!expanded_key_map.values.include?('@value') &&
|
100
|
+
!(expanded_key_map.values == ['@id'])
|
101
|
+
context = context.previous_context if revert_context
|
102
|
+
end
|
103
|
+
|
104
|
+
# Apply property-scoped context after reverting term-scoped context
|
105
|
+
context = context.parse(property_scoped_context, base: base, override_protected: true) unless
|
106
|
+
property_scoped_context.nil?
|
107
|
+
|
108
|
+
# Otherwise, unless the value is a number, expand the value according to the Value Expansion rules, passing active property.
|
109
|
+
unless input.is_a?(Hash)
|
110
|
+
input = context.expand_value(active_property, input, base: base)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Output any type provided from a type map
|
114
|
+
provisional_statements << RDF::Statement(node_id, RDF.type, extra_type) if
|
115
|
+
extra_type
|
116
|
+
|
117
|
+
# Add statement, either provisionally, or just emit
|
118
|
+
add_statement = Proc.new do |st|
|
119
|
+
if have_id || st.to_quad.none? {|r| r == node_id}
|
120
|
+
block.call(st)
|
121
|
+
else
|
122
|
+
provisional_statements << st
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Input is an object (Hash), parse keys in order
|
127
|
+
state = :await_context
|
128
|
+
input.each do |key, value|
|
129
|
+
expanded_key = context.expand_iri(key, base: base, vocab: true)
|
130
|
+
case expanded_key
|
131
|
+
when '@context'
|
132
|
+
raise JsonLdError::InvalidStreamingKeyOrder,
|
133
|
+
"found #{key} in state #{state}" unless state == :await_context
|
134
|
+
context = context.parse(value, base: base)
|
135
|
+
state = :await_type
|
136
|
+
when '@type'
|
137
|
+
# Set the type-scoped context to the context on input, for use later
|
138
|
+
raise JsonLdError::InvalidStreamingKeyOrder,
|
139
|
+
"found #{key} in state #{state}" unless [:await_context, :await_type].include?(state)
|
140
|
+
|
141
|
+
type_scoped_context = context
|
142
|
+
as_array(value).sort.each do |term|
|
143
|
+
raise JsonLdError::InvalidTypeValue,
|
144
|
+
"value of @type must be a string: #{term.inspect}" if !term.is_a?(String)
|
145
|
+
term_context = type_scoped_context.term_definitions[term].context if type_scoped_context.term_definitions[term]
|
146
|
+
context = context.parse(term_context, base: base, propagate: false) unless term_context.nil?
|
147
|
+
type = type_scoped_context.expand_iri(term,
|
148
|
+
base: base,
|
149
|
+
documentRelative: true,
|
150
|
+
vocab: true)
|
151
|
+
|
152
|
+
# Early terminate for @json
|
153
|
+
type = RDF.JSON if type == '@json'
|
154
|
+
# Add a provisional statement
|
155
|
+
provisional_statements << RDF::Statement(node_id, RDF.type, type)
|
156
|
+
end
|
157
|
+
state = :await_type
|
158
|
+
when '@id'
|
159
|
+
raise JsonLdError::InvalidSetOrListObject,
|
160
|
+
"found #{key} in state #{state}" if is_list_or_set
|
161
|
+
raise JsonLdError::CollidingKeywords,
|
162
|
+
"found #{key} in state #{state}" unless [:await_context, :await_type, :await_id].include?(state)
|
163
|
+
|
164
|
+
# Set our actual id, and use for replacing any provisional statements using our existing node_id, which is provisional
|
165
|
+
raise JsonLdError::InvalidIdValue,
|
166
|
+
"value of @id must be a string: #{value.inspect}" if !value.is_a?(String)
|
167
|
+
node_reference = input.keys.length == 1
|
168
|
+
expanded_id = context.expand_iri(value, base: base, documentRelative: true)
|
169
|
+
next if expanded_id.nil?
|
170
|
+
new_node_id = as_resource(expanded_id)
|
171
|
+
# Replace and emit any statements including our provisional id with the newly established node (or graph) id
|
172
|
+
provisional_statements.each do |st|
|
173
|
+
st.subject = new_node_id if st.subject == node_id
|
174
|
+
st.object = new_node_id if st.object == node_id
|
175
|
+
st.graph_name = new_node_id if st.graph_name == node_id
|
176
|
+
block.call(st)
|
177
|
+
end
|
178
|
+
|
179
|
+
provisional_statements.clear
|
180
|
+
have_id, node_id = true, new_node_id
|
181
|
+
|
182
|
+
# if there's a subject & predicate, emit that statement now
|
183
|
+
if subject && predicate
|
184
|
+
st = RDF::Statement(subject, predicate, node_id)
|
185
|
+
block.call(st)
|
186
|
+
end
|
187
|
+
state = :properties
|
188
|
+
|
189
|
+
when '@direction'
|
190
|
+
raise JsonLdError::InvalidStreamingKeyOrder,
|
191
|
+
"found @direction in state #{state}" if state == :properties
|
192
|
+
value_object['@direction'] = value
|
193
|
+
state = :await_id
|
194
|
+
when '@graph'
|
195
|
+
# If `@graph` is at the top level (no `subject`) and value contains no keys other than `@graph` and `@context`, add triples to the default graph
|
196
|
+
# Process all graph statements
|
197
|
+
parse_object(value, nil, context) do |st|
|
198
|
+
# If `@graph` is at the top level (`graph_is_named` is `false`) and input contains no keys other than `@graph` and `@context`, add triples to the default graph
|
199
|
+
relevant_keys = input.keys - ['@context', key]
|
200
|
+
st.graph_name = node_id unless !graph_is_named && relevant_keys.empty?
|
201
|
+
if st.graph_name && !st.graph_name.valid?
|
202
|
+
warn "skipping graph statement within invalid graph name: #{st.inspect}"
|
203
|
+
else
|
204
|
+
add_statement.call(st)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
state = :await_id unless state == :properties
|
208
|
+
when '@included'
|
209
|
+
# Expanded values must be node objects
|
210
|
+
have_statements = false
|
211
|
+
parse_object(value, active_property, context) do |st|
|
212
|
+
have_statements ||= st.has_subject?
|
213
|
+
block.call(st)
|
214
|
+
end
|
215
|
+
raise JsonLdError::InvalidIncludedValue, "values of @included must expand to node objects" unless have_statements
|
216
|
+
state = :await_id unless state == :properties
|
217
|
+
when '@index'
|
218
|
+
state = :await_id unless state == :properties
|
219
|
+
raise JsonLdError::InvalidIndexValue,
|
220
|
+
"Value of @index is not a string: #{value.inspect}" unless value.is_a?(String)
|
221
|
+
when '@language'
|
222
|
+
raise JsonLdError::InvalidStreamingKeyOrder,
|
223
|
+
"found @language in state #{state}" if state == :properties
|
224
|
+
raise JsonLdError::InvalidLanguageTaggedString,
|
225
|
+
"@language value must be a string: #{value.inspect}" if !value.is_a?(String)
|
226
|
+
if value !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
|
227
|
+
warn "@language must be valid BCP47: #{value.inspect}"
|
228
|
+
return
|
229
|
+
end
|
230
|
+
language = value
|
231
|
+
state = :await_id
|
232
|
+
when '@list'
|
233
|
+
raise JsonLdError::InvalidSetOrListObject,
|
234
|
+
"found #{key} in state #{state}" if
|
235
|
+
![:await_context, :await_type, :await_id].include?(state)
|
236
|
+
is_list_or_set = true
|
237
|
+
if subject
|
238
|
+
node_id = parse_list(value, active_property, context, &block)
|
239
|
+
end
|
240
|
+
state = :properties
|
241
|
+
when '@nest'
|
242
|
+
nest_context = context.term_definitions[active_property].context if context.term_definitions[active_property]
|
243
|
+
nest_context = if nest_context.nil?
|
244
|
+
context
|
245
|
+
else
|
246
|
+
context.parse(nest_context, base: base, override_protected: true)
|
247
|
+
end
|
248
|
+
as_array(value).each do |v|
|
249
|
+
raise JsonLdError::InvalidNestValue, v.inspect unless
|
250
|
+
v.is_a?(Hash) && v.keys.none? {|k| nest_context.expand_iri(k, vocab: true, base: base) == '@value'}
|
251
|
+
parse_object(v, active_property, nest_context, node_id: node_id) do |st|
|
252
|
+
add_statement.call(st)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
state = :await_id unless state == :properties
|
256
|
+
when '@reverse'
|
257
|
+
as_array(value).each do |item|
|
258
|
+
item = context.expand_value(active_property, item, base: base) unless item.is_a?(Hash)
|
259
|
+
raise JsonLdError::InvalidReverseValue, item.inspect if value?(item)
|
260
|
+
raise JsonLdError::InvalidReversePropertyMap, item.inspect if node_reference?(item)
|
261
|
+
raise JsonLdError::InvalidReversePropertyValue, item.inspect if list?(item)
|
262
|
+
has_own_subject = false
|
263
|
+
parse_object(item, active_property, context, node_id: node_id, predicate: predicate) do |st|
|
264
|
+
if st.subject == node_id
|
265
|
+
raise JsonLdError::InvalidReversePropertyValue, item.inspect if !st.object.resource?
|
266
|
+
# Invert sense of statements
|
267
|
+
st = RDF::Statement(st.object, st.predicate, st.subject)
|
268
|
+
has_own_subject = true
|
269
|
+
end
|
270
|
+
add_statement.call(st)
|
271
|
+
end
|
272
|
+
|
273
|
+
# If the reversed node does not make any claims on this subject, it's an error
|
274
|
+
raise JsonLdError::InvalidReversePropertyValue, item.inspect unless has_own_subject
|
275
|
+
end
|
276
|
+
state = :await_id unless state == :properties
|
277
|
+
when '@set'
|
278
|
+
raise JsonLdError::InvalidSetOrListObject,
|
279
|
+
"found #{key} in state #{state}" if
|
280
|
+
![:await_context, :await_type, :await_id].include?(state)
|
281
|
+
is_list_or_set = true
|
282
|
+
value = as_array(value).compact
|
283
|
+
parse_object(value, active_property, context, subject: subject, predicate: predicate, &block)
|
284
|
+
node_id = nil
|
285
|
+
state = :properties
|
286
|
+
when '@value'
|
287
|
+
raise JsonLdError::InvalidStreamingKeyOrder,
|
288
|
+
"found @value in state #{state}" if state == :properties
|
289
|
+
value_object['@value'] = value
|
290
|
+
state = :await_id
|
291
|
+
else
|
292
|
+
state = :await_id unless state == :properties
|
293
|
+
# Skip keys that don't expand to a keyword or absolute IRI
|
294
|
+
next if expanded_key.is_a?(RDF::URI) && !expanded_key.absolute?
|
295
|
+
parse_property(value, key, context, node_id, expanded_key) do |st|
|
296
|
+
add_statement.call(st)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Value object with @id
|
302
|
+
raise JsonLdError::InvalidValueObject,
|
303
|
+
"value object has unknown key: @id" if
|
304
|
+
!value_object.empty? && (have_id || is_list_or_set)
|
305
|
+
|
306
|
+
# Can't have both @id and either @list or @set
|
307
|
+
raise JsonLdError::InvalidSetOrListObject,
|
308
|
+
"found @id with @list or @set" if
|
309
|
+
have_id && is_list_or_set
|
310
|
+
|
311
|
+
type_statements = provisional_statements.select {|ps| ps.predicate == RDF.type && ps.graph_name.nil?}
|
312
|
+
value_object['@language'] = (@options[:lowercaseLanguage] ? language.downcase : language) if language
|
313
|
+
if !value_object.empty? &&
|
314
|
+
(!value_object['@value'].nil? ||
|
315
|
+
(type_statements.first || RDF::Statement.new).object == RDF.JSON)
|
316
|
+
|
317
|
+
# There can be only one value of @type
|
318
|
+
case type_statements.length
|
319
|
+
when 0 then #skip
|
320
|
+
when 1
|
321
|
+
raise JsonLdError::InvalidTypedValue,
|
322
|
+
"value of @type must be an IRI or '@json': #{type_statements.first.object.inspect}" unless
|
323
|
+
type_statements.first.object.valid?
|
324
|
+
value_object['@type'] = type_statements.first.object
|
325
|
+
else
|
326
|
+
raise JsonLdError::InvalidValueObject,
|
327
|
+
"value object must not have more than one type"
|
328
|
+
end
|
329
|
+
|
330
|
+
# Check for extra keys
|
331
|
+
raise JsonLdError::InvalidValueObject,
|
332
|
+
"value object has unknown keys: #{value_object.inspect}" unless
|
333
|
+
(value_object.keys - Expand::KEYS_VALUE_LANGUAGE_TYPE_INDEX_DIRECTION).empty?
|
334
|
+
|
335
|
+
# @type is inconsistent with either @language or @direction
|
336
|
+
raise JsonLdError::InvalidValueObject,
|
337
|
+
"value object must not include @type with either " +
|
338
|
+
"@language or @direction: #{value_object.inspect}" if
|
339
|
+
value_object.keys.include?('@type') && !(value_object.keys & %w(@language @direction)).empty?
|
340
|
+
|
341
|
+
if value_object.key?('@language') && !value_object['@value'].is_a?(String)
|
342
|
+
raise JsonLdError::InvalidLanguageTaggedValue,
|
343
|
+
"with @language @value must be a string: #{value_object.inspect}"
|
344
|
+
elsif value_object['@type'] && value_object['@type'] != RDF.JSON
|
345
|
+
raise JsonLdError::InvalidTypedValue,
|
346
|
+
"value of @type must be an IRI or '@json': #{value_object['@type'].inspect}" unless
|
347
|
+
value_object['@type'].is_a?(RDF::URI)
|
348
|
+
elsif value_object['@type'] != RDF.JSON
|
349
|
+
case value_object['@value']
|
350
|
+
when String, TrueClass, FalseClass, Numeric then # okay
|
351
|
+
else
|
352
|
+
raise JsonLdError::InvalidValueObjectValue,
|
353
|
+
"@value is: #{value_object['@value'].inspect}"
|
354
|
+
end
|
355
|
+
end
|
356
|
+
literal = item_to_rdf(value_object, &block)
|
357
|
+
st = RDF::Statement(subject, predicate, literal)
|
358
|
+
block.call(st)
|
359
|
+
elsif !provisional_statements.empty?
|
360
|
+
# Emit all provisional statements, as no @id was ever found
|
361
|
+
provisional_statements.each {|st| block.call(st)}
|
362
|
+
end
|
363
|
+
|
364
|
+
# Use implicit subject to generate the relationship
|
365
|
+
if value_object.empty? && subject && predicate && !have_id && !node_reference
|
366
|
+
block.call(RDF::Statement(subject, predicate, node_id))
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def parse_property(input, active_property, context, subject, predicate, &block)
|
371
|
+
container = context.container(active_property)
|
372
|
+
if container.include?('@language') && input.is_a?(Hash)
|
373
|
+
input.each do |lang, lang_value|
|
374
|
+
expanded_lang = context.expand_iri(lang, vocab: true)
|
375
|
+
if lang !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/ && expanded_lang != '@none'
|
376
|
+
warn "@language must be valid BCP47: #{lang.inspect}"
|
377
|
+
end
|
378
|
+
|
379
|
+
as_array(lang_value).each do |item|
|
380
|
+
raise JsonLdError::InvalidLanguageMapValue,
|
381
|
+
"Expected #{item.inspect} to be a string" unless item.nil? || item.is_a?(String)
|
382
|
+
lang_obj = {'@value' => item}
|
383
|
+
lang_obj['@language'] = lang unless expanded_lang == '@none'
|
384
|
+
lang_obj['@direction'] = context.direction(lang) if context.direction(lang)
|
385
|
+
parse_object(lang_obj, active_property, context, subject: subject, predicate: predicate, &block)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
elsif container.include?('@list')
|
389
|
+
# Handle case where value is a list object
|
390
|
+
if input.is_a?(Hash) &&
|
391
|
+
input.keys.map do |k|
|
392
|
+
context.expand_iri(k, vocab: true, as_string: true, base: base)
|
393
|
+
end.include?('@list')
|
394
|
+
parse_object(input, active_property, context,
|
395
|
+
subject: subject, predicate: predicate, &block)
|
396
|
+
else
|
397
|
+
list = parse_list(input, active_property, context, &block)
|
398
|
+
block.call(RDF::Statement(subject, predicate, list))
|
399
|
+
end
|
400
|
+
elsif container.intersect?(JSON::LD::Expand::CONTAINER_INDEX_ID_TYPE) && input.is_a?(Hash)
|
401
|
+
# Get appropriate context for this container
|
402
|
+
container_context = if container.include?('@type') && context.previous_context
|
403
|
+
context.previous_context
|
404
|
+
elsif container.include?('@id') && context.term_definitions[active_property]
|
405
|
+
id_context = context.term_definitions[active_property].context if context.term_definitions[active_property]
|
406
|
+
if id_context.nil?
|
407
|
+
context
|
408
|
+
else
|
409
|
+
context.parse(id_context, base: base, propagate: false)
|
410
|
+
end
|
411
|
+
else
|
412
|
+
context
|
413
|
+
end
|
414
|
+
|
415
|
+
input.each do |k, v|
|
416
|
+
# If container mapping in the active context includes @type, and k is a term in the active context having a local context, use that context when expanding values
|
417
|
+
map_context = container_context.term_definitions[k].context if
|
418
|
+
container.include?('@type') && container_context.term_definitions[k]
|
419
|
+
unless map_context.nil?
|
420
|
+
map_context = container_context.parse(map_context, base: base, propagate: false)
|
421
|
+
end
|
422
|
+
map_context ||= container_context
|
423
|
+
|
424
|
+
expanded_k = container_context.expand_iri(k, vocab: true, as_string: true, base: base)
|
425
|
+
index_key = context.term_definitions[active_property].index || '@index'
|
426
|
+
|
427
|
+
case
|
428
|
+
when container.include?('@index') && container.include?('@graph')
|
429
|
+
# Index is ignored
|
430
|
+
as_array(v).each do |item|
|
431
|
+
# Each value is in a separate graph
|
432
|
+
graph_name = RDF::Node.new(namer.get_sym)
|
433
|
+
parse_object(item, active_property, context) do |st|
|
434
|
+
st.graph_name ||= graph_name
|
435
|
+
block.call(st)
|
436
|
+
end
|
437
|
+
block.call(RDF::Statement(subject, predicate, graph_name))
|
438
|
+
|
439
|
+
# Add a property index, if appropriate
|
440
|
+
unless index_key == '@index'
|
441
|
+
# Expand key based on term
|
442
|
+
expanded_k = k == '@none' ?
|
443
|
+
'@none' :
|
444
|
+
container_context.expand_value(index_key, k, base: base)
|
445
|
+
|
446
|
+
# Add the index property as a property of the graph name
|
447
|
+
index_property = container_context.expand_iri(index_key, vocab: true, base: base)
|
448
|
+
emit_object(expanded_k, index_key, map_context, graph_name,
|
449
|
+
index_property, from_map: true, &block) unless
|
450
|
+
expanded_k == '@none'
|
451
|
+
end
|
452
|
+
end
|
453
|
+
when container.include?('@index')
|
454
|
+
if index_key == '@index'
|
455
|
+
# Index is ignored
|
456
|
+
emit_object(v, active_property, map_context, subject, predicate, from_map: true, &block)
|
457
|
+
else
|
458
|
+
# Expand key based on term
|
459
|
+
expanded_k = k == '@none' ?
|
460
|
+
'@none' :
|
461
|
+
container_context.expand_value(index_key, k, base: base)
|
462
|
+
|
463
|
+
index_property = container_context.expand_iri(index_key, vocab: true, as_string: true, base: base)
|
464
|
+
|
465
|
+
# index_key is a property
|
466
|
+
as_array(v).each do |item|
|
467
|
+
item = container_context.expand_value(active_property, item, base: base) if item.is_a?(String)
|
468
|
+
raise JsonLdError::InvalidValueObject,
|
469
|
+
"Attempt to add illegal key to value object: #{index_key}" if value?(item)
|
470
|
+
# add expanded_k as value of index_property in item
|
471
|
+
item[index_property] = [expanded_k].concat(Array(item[index_property])) unless expanded_k == '@none'
|
472
|
+
emit_object(item, active_property, map_context, subject, predicate, from_map: true, &block)
|
473
|
+
end
|
474
|
+
end
|
475
|
+
when container.include?('@id') && container.include?('@graph')
|
476
|
+
graph_name = expanded_k == '@none' ?
|
477
|
+
RDF::Node.new(namer.get_sym) :
|
478
|
+
container_context.expand_iri(k, documentRelative: true, base: base)
|
479
|
+
parse_object(v, active_property, context) do |st|
|
480
|
+
st.graph_name ||= graph_name
|
481
|
+
block.call(st)
|
482
|
+
end
|
483
|
+
block.call(RDF::Statement(subject, predicate, graph_name))
|
484
|
+
when container.include?('@id')
|
485
|
+
expanded_k = container_context.expand_iri(k, documentRelative: true, base: base)
|
486
|
+
# pass our id
|
487
|
+
emit_object(v, active_property, map_context, subject, predicate,
|
488
|
+
node_id: (expanded_k unless expanded_k == '@none'),
|
489
|
+
from_map: true,
|
490
|
+
&block)
|
491
|
+
when container.include?('@type')
|
492
|
+
emit_object(v, active_property, map_context, subject, predicate,
|
493
|
+
from_map: true,
|
494
|
+
extra_type: as_resource(expanded_k),
|
495
|
+
&block)
|
496
|
+
end
|
497
|
+
end
|
498
|
+
elsif container.include?('@graph')
|
499
|
+
# Index is ignored
|
500
|
+
as_array(input).each do |v|
|
501
|
+
# Each value is in a separate graph
|
502
|
+
graph_name = RDF::Node.new(namer.get_sym)
|
503
|
+
parse_object(v, active_property, context) do |st|
|
504
|
+
st.graph_name ||= graph_name
|
505
|
+
block.call(st)
|
506
|
+
end
|
507
|
+
block.call(RDF::Statement(subject, predicate, graph_name))
|
508
|
+
end
|
509
|
+
else
|
510
|
+
emit_object(input, active_property, context, subject, predicate, &block)
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
# Wrapps parse_object to handle JSON literals and reversed properties
|
515
|
+
def emit_object(input, active_property, context, subject, predicate, **options, &block)
|
516
|
+
if context.coerce(active_property) == '@json'
|
517
|
+
parse_object(context.expand_value(active_property, input), active_property, context,
|
518
|
+
subject: subject, predicate: predicate, **options, &block)
|
519
|
+
elsif context.reverse?(active_property)
|
520
|
+
as_array(input).each do |item|
|
521
|
+
item = context.expand_value(active_property, item, base: base) unless item.is_a?(Hash)
|
522
|
+
raise JsonLdError::InvalidReverseValue, item.inspect if value?(item)
|
523
|
+
raise JsonLdError::InvalidReversePropertyValue, item.inspect if list?(item)
|
524
|
+
has_own_subject = false
|
525
|
+
parse_object(item, active_property, context, subject: subject, predicate: predicate, **options) do |st|
|
526
|
+
if st.subject == subject
|
527
|
+
raise JsonLdError::InvalidReversePropertyValue, item.inspect if !st.object.resource?
|
528
|
+
# Invert sense of statements
|
529
|
+
st = RDF::Statement(st.object, st.predicate, st.subject)
|
530
|
+
has_own_subject = true
|
531
|
+
end
|
532
|
+
block.call(st)
|
533
|
+
end
|
534
|
+
|
535
|
+
# If the reversed node does not make any claims on this subject, it's an error
|
536
|
+
raise JsonLdError::InvalidReversePropertyValue,
|
537
|
+
"@reverse value must be a node: #{value.inspect}" unless has_own_subject
|
538
|
+
end
|
539
|
+
else
|
540
|
+
as_array(input).flatten.each do |item|
|
541
|
+
# emit property/value
|
542
|
+
parse_object(item, active_property, context,
|
543
|
+
subject: subject, predicate: predicate, **options, &block)
|
544
|
+
end
|
545
|
+
end
|
546
|
+
end
|
547
|
+
|
548
|
+
# Process input as an ordered list
|
549
|
+
# @return [RDF::Resource] the list head
|
550
|
+
def parse_list(input, active_property, context, &block)
|
551
|
+
# Transform all entries into their values
|
552
|
+
# this allows us to eliminate those that don't create any statements
|
553
|
+
fake_subject = RDF::Node.new
|
554
|
+
values = as_array(input).map do |entry|
|
555
|
+
if entry.is_a?(Array)
|
556
|
+
# recursive list
|
557
|
+
entry_value = parse_list(entry, active_property, context, &block)
|
558
|
+
else
|
559
|
+
entry_value = nil
|
560
|
+
parse_object(entry, active_property, context, subject: fake_subject, predicate: RDF.first) do |st|
|
561
|
+
if st.subject == fake_subject
|
562
|
+
entry_value = st.object
|
563
|
+
else
|
564
|
+
block.call(st)
|
565
|
+
end
|
566
|
+
end
|
567
|
+
entry_value
|
568
|
+
end
|
569
|
+
end.compact
|
570
|
+
return RDF.nil if values.empty?
|
571
|
+
|
572
|
+
# Construct a list from values, and emit list statements, returning the list subject
|
573
|
+
list = RDF::List(*values)
|
574
|
+
list.each_statement(&block)
|
575
|
+
return list.subject
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|