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.
@@ -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