json-ld 3.1.2 → 3.1.7
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 +124 -48
- data/VERSION +1 -1
- data/bin/jsonld +27 -30
- data/lib/json/ld.rb +6 -2
- data/lib/json/ld/api.rb +33 -24
- data/lib/json/ld/compact.rb +65 -37
- data/lib/json/ld/conneg.rb +1 -1
- data/lib/json/ld/context.rb +612 -539
- data/lib/json/ld/expand.rb +158 -77
- data/lib/json/ld/format.rb +20 -7
- data/lib/json/ld/from_rdf.rb +40 -17
- data/lib/json/ld/reader.rb +20 -11
- data/lib/json/ld/streaming_reader.rb +578 -0
- data/lib/json/ld/to_rdf.rb +9 -5
- data/lib/json/ld/writer.rb +10 -3
- data/spec/compact_spec.rb +207 -2
- data/spec/context_spec.rb +13 -60
- data/spec/expand_spec.rb +248 -0
- data/spec/from_rdf_spec.rb +181 -0
- data/spec/matchers.rb +1 -1
- data/spec/reader_spec.rb +33 -34
- data/spec/streaming_reader_spec.rb +237 -0
- data/spec/suite_helper.rb +14 -8
- data/spec/suite_to_rdf_spec.rb +1 -0
- data/spec/to_rdf_spec.rb +206 -0
- data/spec/writer_spec.rb +193 -0
- metadata +9 -6
data/lib/json/ld/format.rb
CHANGED
@@ -58,9 +58,10 @@ module JSON::LD
|
|
58
58
|
out = options[:output] || $stdout
|
59
59
|
out.set_encoding(Encoding::UTF_8) if RUBY_PLATFORM == "java"
|
60
60
|
options = options.merge(expandContext: options.delete(:context)) if options.has_key?(:context)
|
61
|
+
options[:base] ||= options[:base_uri]
|
61
62
|
if options[:format] == :jsonld
|
62
63
|
if files.empty?
|
63
|
-
# If files are empty, either use options[:
|
64
|
+
# If files are empty, either use options[:evaluate] or STDIN
|
64
65
|
input = options[:evaluate] ? StringIO.new(options[:evaluate]) : STDIN
|
65
66
|
input.set_encoding(options.fetch(:encoding, Encoding::UTF_8))
|
66
67
|
JSON::LD::API.expand(input, validate: false, **options) do |expanded|
|
@@ -93,9 +94,10 @@ module JSON::LD
|
|
93
94
|
raise ArgumentError, "Compacting requires a context" unless options[:context]
|
94
95
|
out = options[:output] || $stdout
|
95
96
|
out.set_encoding(Encoding::UTF_8) if RUBY_PLATFORM == "java"
|
97
|
+
options[:base] ||= options[:base_uri]
|
96
98
|
if options[:format] == :jsonld
|
97
99
|
if files.empty?
|
98
|
-
# If files are empty, either use options[:
|
100
|
+
# If files are empty, either use options[:evaluate] or STDIN
|
99
101
|
input = options[:evaluate] ? StringIO.new(options[:evaluate]) : STDIN
|
100
102
|
input.set_encoding(options.fetch(:encoding, Encoding::UTF_8))
|
101
103
|
JSON::LD::API.compact(input, options[:context], **options) do |compacted|
|
@@ -126,7 +128,7 @@ module JSON::LD
|
|
126
128
|
control: :url2,
|
127
129
|
use: :required,
|
128
130
|
on: ["--context CONTEXT"],
|
129
|
-
description: "Context to use when compacting.") {|arg| RDF::URI(arg)},
|
131
|
+
description: "Context to use when compacting.") {|arg| RDF::URI(arg).absolute? ? RDF::URI(arg) : StringIO.new(File.read(arg))},
|
130
132
|
]
|
131
133
|
},
|
132
134
|
flatten: {
|
@@ -137,9 +139,10 @@ module JSON::LD
|
|
137
139
|
lambda: ->(files, **options) do
|
138
140
|
out = options[:output] || $stdout
|
139
141
|
out.set_encoding(Encoding::UTF_8) if RUBY_PLATFORM == "java"
|
142
|
+
options[:base] ||= options[:base_uri]
|
140
143
|
if options[:format] == :jsonld
|
141
144
|
if files.empty?
|
142
|
-
# If files are empty, either use options[:
|
145
|
+
# If files are empty, either use options[:evaluate] or STDIN
|
143
146
|
input = options[:evaluate] ? StringIO.new(options[:evaluate]) : STDIN
|
144
147
|
input.set_encoding(options.fetch(:encoding, Encoding::UTF_8))
|
145
148
|
JSON::LD::API.flatten(input, options[:context], **options) do |flattened|
|
@@ -162,7 +165,16 @@ module JSON::LD
|
|
162
165
|
end
|
163
166
|
end
|
164
167
|
end
|
165
|
-
end
|
168
|
+
end,
|
169
|
+
options: [
|
170
|
+
RDF::CLI::Option.new(
|
171
|
+
symbol: :context,
|
172
|
+
datatype: RDF::URI,
|
173
|
+
control: :url2,
|
174
|
+
use: :required,
|
175
|
+
on: ["--context CONTEXT"],
|
176
|
+
description: "Context to use when compacting.") {|arg| RDF::URI(arg)},
|
177
|
+
]
|
166
178
|
},
|
167
179
|
frame: {
|
168
180
|
description: "Frame JSON-LD or parsed RDF",
|
@@ -173,9 +185,10 @@ module JSON::LD
|
|
173
185
|
raise ArgumentError, "Framing requires a frame" unless options[:frame]
|
174
186
|
out = options[:output] || $stdout
|
175
187
|
out.set_encoding(Encoding::UTF_8) if RUBY_PLATFORM == "java"
|
188
|
+
options[:base] ||= options[:base_uri]
|
176
189
|
if options[:format] == :jsonld
|
177
190
|
if files.empty?
|
178
|
-
# If files are empty, either use options[:
|
191
|
+
# If files are empty, either use options[:evaluate] or STDIN
|
179
192
|
input = options[:evaluate] ? StringIO.new(options[:evaluate]) : STDIN
|
180
193
|
input.set_encoding(options.fetch(:encoding, Encoding::UTF_8))
|
181
194
|
JSON::LD::API.frame(input, options[:frame], **options) do |framed|
|
@@ -207,7 +220,7 @@ module JSON::LD
|
|
207
220
|
control: :url2,
|
208
221
|
use: :required,
|
209
222
|
on: ["--frame FRAME"],
|
210
|
-
description: "Frame to use when serializing.") {|arg| RDF::URI(arg)}
|
223
|
+
description: "Frame to use when serializing.") {|arg| RDF::URI(arg).absolute? ? RDF::URI(arg) : StringIO.new(File.read(arg))}
|
211
224
|
]
|
212
225
|
},
|
213
226
|
}
|
data/lib/json/ld/from_rdf.rb
CHANGED
@@ -14,16 +14,14 @@ 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 = {}
|
24
23
|
|
25
24
|
value = nil
|
26
|
-
ec = @context
|
27
25
|
|
28
26
|
# Create an entry for compound-literal node detection
|
29
27
|
compound_literal_subjects = {}
|
@@ -34,7 +32,7 @@ module JSON::LD
|
|
34
32
|
dataset.each do |statement|
|
35
33
|
#log_debug("statement") { statement.to_nquads.chomp}
|
36
34
|
|
37
|
-
name = statement.graph_name ?
|
35
|
+
name = statement.graph_name ? @context.expand_iri(statement.graph_name, base: @options[:base]).to_s : '@default'
|
38
36
|
|
39
37
|
# Create a graph entry as needed
|
40
38
|
node_map = graph_map[name] ||= {}
|
@@ -42,30 +40,29 @@ module JSON::LD
|
|
42
40
|
|
43
41
|
default_graph[name] ||= {'@id' => name} unless name == '@default'
|
44
42
|
|
45
|
-
subject =
|
46
|
-
node = node_map[subject] ||=
|
43
|
+
subject = statement.subject.to_s
|
44
|
+
node = node_map[subject] ||= resource_representation(statement.subject,useNativeTypes)
|
47
45
|
|
48
46
|
# If predicate is rdf:datatype, note subject in compound literal subjects map
|
49
47
|
if @options[:rdfDirection] == 'compound-literal' && statement.predicate == RDF.to_uri + 'direction'
|
50
48
|
compound_literal_subjects[name][subject] ||= true
|
51
49
|
end
|
52
50
|
|
53
|
-
# If object is an IRI
|
54
|
-
|
55
|
-
statement.object.
|
51
|
+
# If object is an IRI, blank node identifier, or statement, and node map does not have an object member, create one and initialize its value to a new JSON object consisting of a single member @id whose value is set to object.
|
52
|
+
unless statement.object.literal?
|
53
|
+
node_map[statement.object.to_s] ||=
|
54
|
+
resource_representation(statement.object, useNativeTypes)
|
55
|
+
end
|
56
56
|
|
57
57
|
# If predicate equals rdf:type, and object is an IRI or blank node identifier, append object to the value of the @type member of node. If no such member exists, create one and initialize it to an array whose only item is object. Finally, continue to the next RDF triple.
|
58
|
+
# XXX JSON-LD* does not support embedded value of @type
|
58
59
|
if statement.predicate == RDF.type && statement.object.resource? && !useRdfType
|
59
60
|
merge_value(node, '@type', statement.object.to_s)
|
60
61
|
next
|
61
62
|
end
|
62
63
|
|
63
64
|
# Set value to the result of using the RDF to Object Conversion algorithm, passing object, rdfDirection, and use native types.
|
64
|
-
value =
|
65
|
-
statement.object,
|
66
|
-
rdfDirection: @options[:rdfDirection],
|
67
|
-
useNativeTypes: useNativeTypes,
|
68
|
-
log_depth: @options[:log_depth])
|
65
|
+
value = resource_representation(statement.object, useNativeTypes)
|
69
66
|
|
70
67
|
merge_value(node, statement.predicate.to_s, value)
|
71
68
|
|
@@ -147,11 +144,11 @@ module JSON::LD
|
|
147
144
|
end
|
148
145
|
|
149
146
|
result = []
|
150
|
-
default_graph.keys.opt_sort(ordered: ordered).each do |subject|
|
147
|
+
default_graph.keys.opt_sort(ordered: @options[:ordered]).each do |subject|
|
151
148
|
node = default_graph[subject]
|
152
149
|
if graph_map.has_key?(subject)
|
153
150
|
node['@graph'] = []
|
154
|
-
graph_map[subject].keys.opt_sort(ordered: ordered).each do |s|
|
151
|
+
graph_map[subject].keys.opt_sort(ordered: @options[:ordered]).each do |s|
|
155
152
|
n = graph_map[subject][s]
|
156
153
|
n.delete(:usages)
|
157
154
|
node['@graph'] << n unless node_reference?(n)
|
@@ -163,5 +160,31 @@ module JSON::LD
|
|
163
160
|
#log_debug("fromRdf") {result.to_json(JSON_STATE) rescue 'malformed json'}
|
164
161
|
result
|
165
162
|
end
|
163
|
+
|
164
|
+
private
|
165
|
+
def resource_representation(resource, useNativeTypes)
|
166
|
+
case resource
|
167
|
+
when RDF::Statement
|
168
|
+
# Note, if either subject or object are a BNode which is used elsewhere,
|
169
|
+
# this might not work will with the BNode accounting from above.
|
170
|
+
rep = {'@id' => resource_representation(resource.subject, false)}
|
171
|
+
if resource.predicate == RDF.type
|
172
|
+
rep['@id'].merge!('@type' => resource.object.to_s)
|
173
|
+
else
|
174
|
+
rep['@id'].merge!(
|
175
|
+
resource.predicate.to_s =>
|
176
|
+
as_array(resource_representation(resource.object, useNativeTypes)))
|
177
|
+
end
|
178
|
+
rep
|
179
|
+
when RDF::Literal
|
180
|
+
@context.expand_value(nil,
|
181
|
+
resource,
|
182
|
+
rdfDirection: @options[:rdfDirection],
|
183
|
+
useNativeTypes: useNativeTypes,
|
184
|
+
base: @options[:base])
|
185
|
+
else
|
186
|
+
{'@id' => resource.to_s}
|
187
|
+
end
|
188
|
+
end
|
166
189
|
end
|
167
190
|
end
|
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
|
##
|
@@ -19,7 +20,7 @@ module JSON::LD
|
|
19
20
|
control: :url2,
|
20
21
|
datatype: RDF::URI,
|
21
22
|
on: ["--expand-context CONTEXT"],
|
22
|
-
description: "Context to use when expanding.") {|arg| RDF::URI(arg)},
|
23
|
+
description: "Context to use when expanding.") {|arg| RDF::URI(arg).absolute? ? RDF::URI(arg) : StringIO.new(File.read(arg))},
|
23
24
|
RDF::CLI::Option.new(
|
24
25
|
symbol: :extractAllScripts,
|
25
26
|
datatype: TrueClass,
|
@@ -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
|