json-ld 3.1.2 → 3.1.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|