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.
@@ -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[:execute]
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[:execute]
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[:execute]
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[:execute]
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
  }
@@ -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
- # @param [Boolean] ordered (true)
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, ordered: 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 ? ec.expand_iri(statement.graph_name).to_s : '@default'
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 = ec.expand_iri(statement.subject, as_string: true)
46
- node = node_map[subject] ||= {'@id' => 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 or blank node identifier, 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.
54
- node_map[statement.object.to_s] ||= {'@id' => statement.object.to_s} unless
55
- statement.object.literal?
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 = ec.expand_value(nil,
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
@@ -1,5 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # frozen_string_literal: true
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
- begin
67
- # Trim non-JSON stuff in script.
68
- @doc = if input.respond_to?(:read)
69
- input
70
- else
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
- JSON::LD::API.toRdf(@doc, **@options, &block)
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
- JSON::LD::API.toRdf(@doc, **@options) do |statement|
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