rdf 3.1.1 → 3.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -400,7 +400,7 @@ module RDF
400
400
  # @example Joining two URIs
401
401
  # RDF::URI.new('http://example.org/foo/bar').join('/foo')
402
402
  # #=> RDF::URI('http://example.org/foo')
403
- # @see <http://github.com/ruby-rdf/rdf-spec/blob/master/lib/rdf/spec/uri.rb>
403
+ # @see <https://github.com/ruby-rdf/rdf-spec/blob/master/lib/rdf/spec/uri.rb>
404
404
  # @see <http://tools.ietf.org/html/rfc3986#section-5.2>
405
405
  # @see RDF::URI#/
406
406
  # @see RDF::URI#+
@@ -471,7 +471,7 @@ module RDF
471
471
  # @see RDF::URI#+
472
472
  # @see RDF::URI#join
473
473
  # @see <http://tools.ietf.org/html/rfc3986#section-5.2>
474
- # @see <http://github.com/ruby-rdf/rdf-spec/blob/master/lib/rdf/spec/uri.rb>
474
+ # @see <https://github.com/ruby-rdf/rdf-spec/blob/master/lib/rdf/spec/uri.rb>
475
475
  # @example Building a HTTP URL
476
476
  # RDF::URI.new('http://example.org') / 'jhacker' / 'foaf.ttl'
477
477
  # #=> RDF::URI('http://example.org/jhacker/foaf.ttl')
@@ -69,9 +69,9 @@ module RDF
69
69
 
70
70
  begin
71
71
  unless blank? || read_comment
72
- subject = read_uriref || read_node || fail_subject
72
+ subject = read_uriref || read_node || read_rdfstar || fail_subject
73
73
  predicate = read_uriref(intern: true) || fail_predicate
74
- object = read_uriref || read_node || read_literal || fail_object
74
+ object = read_uriref || read_node || read_literal || read_rdfstar || fail_object
75
75
  graph_name = read_uriref || read_node
76
76
  if validate? && !read_eos
77
77
  log_error("Expected end of statement (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError)
@@ -15,15 +15,17 @@ module RDF
15
15
  #
16
16
  # <https://rubygems.org/gems/rdf> <http://purl.org/dc/terms/title> "rdf" .
17
17
  #
18
- # Installation
19
- # ------------
18
+ # ## RDFStar (RDF*)
19
+ #
20
+ # Supports statements as resources using `<<s p o>>`.
21
+ #
22
+ # ## Installation
20
23
  #
21
24
  # This is the only RDF serialization format that is directly supported by
22
25
  # RDF.rb. Support for other formats is available in the form of add-on
23
26
  # gems, e.g. 'rdf-xml' or 'rdf-json'.
24
27
  #
25
- # Documentation
26
- # -------------
28
+ # ## Documentation
27
29
  #
28
30
  # * {RDF::NTriples::Format}
29
31
  # * {RDF::NTriples::Reader}
@@ -25,6 +25,10 @@ module RDF::NTriples
25
25
  # end
26
26
  # end
27
27
  #
28
+ # ** RDFStar (RDF*)
29
+ #
30
+ # Supports statements as resources using `<<s p o>>`.
31
+ #
28
32
  # @see http://www.w3.org/TR/rdf-testcases/#ntriples
29
33
  # @see http://www.w3.org/TR/n-triples/
30
34
  class Reader < RDF::Reader
@@ -70,6 +74,10 @@ module RDF::NTriples
70
74
  # 22
71
75
  STRING_LITERAL_QUOTE = /"((?:[^\"\\\n\r]|#{ECHAR}|#{UCHAR})*)"/.freeze
72
76
 
77
+ # RDF*
78
+ ST_START = /^<</.freeze
79
+ ST_END = /^\s*>>/.freeze
80
+
73
81
  # @see http://www.w3.org/TR/rdf-testcases/#ntrip_grammar
74
82
  COMMENT = /^#\s*(.*)$/.freeze
75
83
  NODEID = /^#{BLANK_NODE_LABEL}/.freeze
@@ -202,7 +210,7 @@ module RDF::NTriples
202
210
  begin
203
211
  read_statement
204
212
  rescue RDF::ReaderError
205
- value = read_uriref || read_node || read_literal
213
+ value = read_uriref || read_node || read_literal || read_rdfstar
206
214
  log_recover
207
215
  value
208
216
  end
@@ -218,9 +226,9 @@ module RDF::NTriples
218
226
 
219
227
  begin
220
228
  unless blank? || read_comment
221
- subject = read_uriref || read_node || fail_subject
229
+ subject = read_uriref || read_node || read_rdfstar || fail_subject
222
230
  predicate = read_uriref(intern: true) || fail_predicate
223
- object = read_uriref || read_node || read_literal || fail_object
231
+ object = read_uriref || read_node || read_literal || read_rdfstar || fail_object
224
232
 
225
233
  if validate? && !read_eos
226
234
  log_error("Expected end of statement (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError)
@@ -234,6 +242,20 @@ module RDF::NTriples
234
242
  end
235
243
  end
236
244
 
245
+ ##
246
+ # @return [RDF::Statement]
247
+ def read_rdfstar
248
+ if @options[:rdfstar] && match(ST_START)
249
+ subject = read_uriref || read_node || read_rdfstar || fail_subject
250
+ predicate = read_uriref(intern: true) || fail_predicate
251
+ object = read_uriref || read_node || read_literal || read_rdfstar || fail_object
252
+ if !match(ST_END)
253
+ log_error("Expected end of statement (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError)
254
+ end
255
+ RDF::Statement.new(subject, predicate, object)
256
+ end
257
+ end
258
+
237
259
  ##
238
260
  # @return [Boolean]
239
261
  # @see http://www.w3.org/TR/rdf-testcases/#ntrip_grammar (comment)
@@ -221,6 +221,15 @@ module RDF::NTriples
221
221
  format_triple(*statement.to_triple, **options)
222
222
  end
223
223
 
224
+ ##
225
+ # Returns the N-Triples representation of an RDF* reified statement.
226
+ #
227
+ # @param [RDF::Statement] statement
228
+ # @param [Hash{Symbol => Object}] options ({})
229
+ # @return [String]
230
+ def format_rdfstar(statement, **options)
231
+ "<<%s %s %s>>" % statement.to_a.map { |value| format_term(value, **options) }
232
+ end
224
233
  ##
225
234
  # Returns the N-Triples representation of a triple.
226
235
  #
@@ -289,6 +289,8 @@ module RDF
289
289
  # any additional keyword options
290
290
  # @option options [Hash{Symbol => RDF::Term}] bindings
291
291
  # optional variable bindings to use
292
+ # @option options [Boolean] :optimize
293
+ # Optimize query before execution.
292
294
  # @option options [RDF::Query::Solutions] solutions
293
295
  # optional initial solutions for chained queries
294
296
  # @yield [solution]
@@ -311,6 +313,7 @@ module RDF
311
313
  return @solutions
312
314
  end
313
315
 
316
+ self.optimize! if options[:optimize]
314
317
  patterns = @patterns
315
318
  graph_name = name if graph_name.nil?
316
319
  @graph_name = graph_name unless graph_name.nil?
@@ -505,7 +508,7 @@ module RDF
505
508
  # @return [RDF::Query]
506
509
  def dup
507
510
  patterns = @patterns.map {|p| p.dup}
508
- Query.new(patterns, solutions: @solutions.dup, **options)
511
+ Query.new(patterns, graph_name: graph_name, solutions: @solutions.dup, **options)
509
512
  end
510
513
 
511
514
  ##
@@ -77,14 +77,15 @@ module RDF; class Query
77
77
  ##
78
78
  # Returns the normalization of the specified `hash_pattern`.
79
79
  #
80
- # @param [Hash{Symbol => Object}] hash_pattern (Hash.new)
81
- # the query pattern as a hash.
82
- # @param [Hash{Symbol => Object}] options (Hash.new)
83
- # any additional normalization options.
84
- # @option options [String] :anonymous_subject_format ("__%s__")
85
- # the string format for anonymous subjects.
86
- # @return [Hash{Symbol => Object}]
87
- # the resulting query pattern as a normalized hash.
80
+ # @overload normalize!(hash_pattern, **options)
81
+ # @param [Hash{Symbol => Object}] hash_pattern (Hash.new)
82
+ # the query pattern as a hash.
83
+ # @param [Hash{Symbol => Object}] **options
84
+ # any additional normalization options.
85
+ # @option options [String] :anonymous_subject_format ("__%s__")
86
+ # the string format for anonymous subjects.
87
+ # @return [Hash{Symbol => Object}]
88
+ # the resulting query pattern as a normalized hash.
88
89
  def normalize!(*args)
89
90
  hash_pattern = args.shift
90
91
  options = args.shift || {}
@@ -50,10 +50,12 @@ module RDF; class Query
50
50
 
51
51
  # Estmate cost positionally, with variables being least expensive as objects, then predicates, then subjects, then graph_names.
52
52
  # XXX does not consider bound variables, which would need to be dynamically calculated.
53
- @cost = (@object.nil? || @object.is_a?(Variable) ? 1 : 0) +
54
- (@predicate.nil? || @predicate.is_a?(Variable) ? 2 : 0) +
55
- (@subject.nil? || @subject.is_a?(Variable) ? 4 : 0) +
56
- (@graph_name.is_a?(Variable) ? 8 : 0)
53
+ @cost = (@object.nil? || @object.is_a?(Variable) ? 8 : 0) +
54
+ (@predicate.nil? || @predicate.is_a?(Variable) ? 4 : 0) +
55
+ (@subject.nil? || @subject.is_a?(Variable) ? 2 : 0) +
56
+ (@graph_name.is_a?(Variable) ? 1 : 0) +
57
+ (@object.is_a?(Pattern) ? (@object.cost * 4) : 0) +
58
+ (@subject.is_a?(Pattern) ? (@subject.cost * 2) : 0)
57
59
  super
58
60
  end
59
61
 
@@ -84,10 +86,10 @@ module RDF; class Query
84
86
  # @return [Boolean] `true` or `false`
85
87
  # @since 0.3.0
86
88
  def has_variables?
87
- subject.is_a?(Variable) ||
88
- predicate.is_a?(Variable) ||
89
- object.is_a?(Variable) ||
90
- graph_name.is_a?(Variable)
89
+ subject && subject.variable? ||
90
+ predicate && predicate.variable? ||
91
+ object && object.variable? ||
92
+ graph_name && graph_name.variable?
91
93
  end
92
94
  alias_method :variables?, :has_variables?
93
95
 
@@ -117,13 +119,33 @@ module RDF; class Query
117
119
  false
118
120
  end
119
121
 
122
+ ##
123
+ # Checks pattern equality against a statement, considering nesting.
124
+ #
125
+ # * A pattern which has a pattern as a subject or an object, matches
126
+ # a statement having a statement as a subject or an object using {#eql?}.
127
+ #
128
+ # @param [Statement] other
129
+ # @return [Boolean]
130
+ #
131
+ # @see RDF::URI#==
132
+ # @see RDF::Node#==
133
+ # @see RDF::Literal#==
134
+ # @see RDF::Query::Variable#==
135
+ def eql?(other)
136
+ return false unless other.is_a?(Statement) && (self.graph_name || false) == (other.graph_name || false)
137
+
138
+ predicate == other.predicate &&
139
+ (subject.is_a?(Pattern) ? subject.eql?(other.subject) : subject == other.subject) &&
140
+ (object.is_a?(Pattern) ? object.eql?(other.object) : object == other.object)
141
+ end
142
+
120
143
  ##
121
144
  # Executes this query pattern on the given `queryable` object.
122
145
  #
123
146
  # Values are matched using using Queryable#query_pattern.
124
147
  #
125
- # If the optional `bindings` are given, variables will be substituted with their values
126
- # when executing the query.
148
+ # If the optional `bindings` are given, variables will be substituted with their values when executing the query.
127
149
  #
128
150
  # To match triples only in the default graph, set graph_name to `false`.
129
151
  #
@@ -198,6 +220,8 @@ module RDF; class Query
198
220
  solution[predicate.to_sym] = statement.predicate if predicate.is_a?(Variable)
199
221
  solution[object.to_sym] = statement.object if object.is_a?(Variable)
200
222
  solution[graph_name.to_sym] = statement.graph_name if graph_name.is_a?(Variable)
223
+ solution.merge!(subject.solution(statement.subject)) if subject.is_a?(Pattern)
224
+ solution.merge!(object.solution(statement.object)) if object.is_a?(Pattern)
201
225
  end
202
226
  end
203
227
 
@@ -229,7 +253,8 @@ module RDF; class Query
229
253
  # @return [Integer] (0..3)
230
254
  def variable_count
231
255
  [subject, predicate, object, graph_name].inject(0) do |memo, term|
232
- memo += (term.is_a?(Variable) ? 1 : 0)
256
+ memo += (term.is_a?(Variable) ? 1 :
257
+ (term.is_a?(Pattern) ? term.variable_count : 0))
233
258
  end
234
259
  end
235
260
  alias_method :cardinality, :variable_count
@@ -243,7 +268,7 @@ module RDF; class Query
243
268
  # @return [Hash{Symbol => Variable}]
244
269
  def variables
245
270
  [subject, predicate, object, graph_name].inject({}) do |memo, term|
246
- term.is_a?(Variable) ? memo.merge(term.variables) : memo
271
+ term && term.variable? ? memo.merge(term.variables) : memo
247
272
  end
248
273
  end
249
274
 
@@ -254,8 +279,10 @@ module RDF; class Query
254
279
  # @return [self]
255
280
  def bind(solution)
256
281
  self.to_quad.each_with_index do |term, index|
257
- if term && term.variable? && solution[term]
282
+ if term.is_a?(Variable) && solution[term]
258
283
  self[index] = solution[term]
284
+ elsif term.is_a?(Pattern)
285
+ term.bind(solution)
259
286
  end
260
287
  end
261
288
  self
@@ -283,9 +310,9 @@ module RDF; class Query
283
310
  # @return [Hash{Symbol => RDF::Term}]
284
311
  def bindings
285
312
  bindings = {}
286
- bindings.merge!(subject.bindings) if subject.is_a?(Variable)
313
+ bindings.merge!(subject.bindings) if subject && subject.variable?
287
314
  bindings.merge!(predicate.bindings) if predicate.is_a?(Variable)
288
- bindings.merge!(object.bindings) if object.is_a?(Variable)
315
+ bindings.merge!(object.bindings) if object && object.variable?
289
316
  bindings.merge!(graph_name.bindings) if graph_name.is_a?(Variable)
290
317
  bindings
291
318
  end
@@ -330,7 +357,13 @@ module RDF; class Query
330
357
  StringIO.open do |buffer| # FIXME in RDF::Statement
331
358
  buffer << 'OPTIONAL ' if optional?
332
359
  buffer << [subject, predicate, object].map do |r|
333
- r.is_a?(RDF::Query::Variable) ? r.to_s : RDF::NTriples.serialize(r)
360
+ if r.is_a?(RDF::Query::Variable)
361
+ r.to_s
362
+ elsif r.is_a?(RDF::Query::Pattern)
363
+ "<<#{r.to_s[0..-3]}>>"
364
+ else
365
+ RDF::NTriples.serialize(r)
366
+ end
334
367
  end.join(" ")
335
368
  buffer << case graph_name
336
369
  when nil, false then " ."
@@ -195,12 +195,31 @@ class RDF::Query
195
195
  # Merges the bindings from the given `other` query solution into this
196
196
  # one, overwriting any existing ones having the same name.
197
197
  #
198
+ # ## RDFStar (RDF*)
199
+ #
200
+ # If merging a binding for a statement to a pattern,
201
+ # merge their embedded solutions.
202
+ #
198
203
  # @param [RDF::Query::Solution, #to_h] other
199
204
  # another query solution or hash bindings
200
205
  # @return [void] self
201
206
  # @since 0.3.0
202
207
  def merge!(other)
203
- @bindings.merge!(other.to_h)
208
+ @bindings.merge!(other.to_h) do |key, v1, v2|
209
+ # Don't merge a pattern over a statement
210
+ # This happens because JOIN does a reverse merge,
211
+ # and a pattern is set in v2.
212
+ v2.is_a?(Pattern) ? v1 : v2
213
+ end
214
+ # Merge bindings from patterns
215
+ embedded_solutions = []
216
+ @bindings.each do |k, v|
217
+ if v.is_a?(Pattern) && other[k].is_a?(RDF::Statement)
218
+ embedded_solutions << v.solution(other[k])
219
+ end
220
+ end
221
+ # Merge embedded solutions
222
+ embedded_solutions.each {|soln| merge!(soln)}
204
223
  self
205
224
  end
206
225
 
@@ -66,13 +66,15 @@ module RDF; class Query
66
66
  #
67
67
  # @return [Array<Symbol>]
68
68
  def variable_names
69
- variables = self.inject({}) do |result, solution|
70
- solution.each_name do |name|
71
- result[name] ||= true
69
+ @variable_names ||= begin
70
+ variables = self.inject({}) do |result, solution|
71
+ solution.each_name do |name|
72
+ result[name] ||= true
73
+ end
74
+ result
72
75
  end
73
- result
76
+ variables.keys
74
77
  end
75
- variables.keys
76
78
  end
77
79
 
78
80
  ##
@@ -136,6 +138,7 @@ module RDF; class Query
136
138
  # @yieldreturn [Boolean]
137
139
  # @return [self]
138
140
  def filter(criteria = {})
141
+ @variable_names = nil
139
142
  if block_given?
140
143
  self.reject! do |solution|
141
144
  !yield(solution.is_a?(Solution) ? solution : Solution.new(solution))
@@ -223,6 +226,13 @@ module RDF; class Query
223
226
  solution.bindings.delete_if { |k, v| !variables.include?(k.to_sym) }
224
227
  end
225
228
  end
229
+
230
+ # Make sure variable_names are ordered by projected variables
231
+ projected_vars, vars = variables.map(&:to_sym), variable_names
232
+ vars = variable_names
233
+
234
+ # Maintain projected order, and add any non-projected variables
235
+ @variable_names = (projected_vars & vars) + (vars - projected_vars)
226
236
  self
227
237
  end
228
238
  alias_method :select, :project
@@ -157,12 +157,24 @@ class RDF::Query
157
157
  ##
158
158
  # Rebinds this variable to the given `value`.
159
159
  #
160
- # @param [RDF::Term] value
161
- # @return [RDF::Term] the previous value, if any.
160
+ # @overload bind(value)
161
+ # @param [RDF::Query::Solution] value
162
+ # @return [self] the bound variable
163
+ #
164
+ # @overload bind(value)
165
+ # @param [RDF::Term] value
166
+ # @return [RDF::Term] the previous value, if any.
162
167
  def bind(value)
163
- old_value = self.value
164
- self.value = value
165
- old_value
168
+ if value.is_a?(RDF::Query::Solution)
169
+ self.value = value.to_h.fetch(name, self.value)
170
+ self
171
+ else
172
+ warn "[DEPRECATION] RDF::Query::Variable#bind should be used with a solution, not a term.\n" +
173
+ "Called from #{Gem.location_of_caller.join(':')}"
174
+ old_value = self.value
175
+ self.value = value
176
+ old_value
177
+ end
166
178
  end
167
179
  alias_method :bind!, :bind
168
180
 
@@ -121,6 +121,12 @@ module RDF
121
121
  # @return [Array<RDF::CLI::Option>]
122
122
  def self.options
123
123
  [
124
+ RDF::CLI::Option.new(
125
+ symbol: :base_uri,
126
+ control: :url,
127
+ datatype: RDF::URI,
128
+ on: ["--uri URI"],
129
+ description: "Base URI of input file, defaults to the filename.") {|arg| RDF::URI(arg)},
124
130
  RDF::CLI::Option.new(
125
131
  symbol: :canonicalize,
126
132
  datatype: TrueClass,
@@ -153,11 +159,11 @@ module RDF
153
159
  end
154
160
  end,
155
161
  RDF::CLI::Option.new(
156
- symbol: :base_uri,
157
- control: :url,
158
- datatype: RDF::URI,
159
- on: ["--uri URI"],
160
- description: "Base URI of input file, defaults to the filename.") {|arg| RDF::URI(arg)},
162
+ symbol: :rdfstar,
163
+ control: :select,
164
+ datatype: [:PG, :SA],
165
+ on: ["--rdf-star MODE"],
166
+ description: "Parse RDF*, either in Property Graph mode (PG) or Separate Assertions mode (SA).") {|arg| arg.to_sym},
161
167
  RDF::CLI::Option.new(
162
168
  symbol: :validate,
163
169
  datatype: TrueClass,
@@ -261,42 +267,48 @@ module RDF
261
267
  #
262
268
  # @param [IO, File, String] input
263
269
  # the input stream to read
264
- # @param [Encoding] encoding (Encoding::UTF_8)
265
- # the encoding of the input stream
266
- # @param [Boolean] validate (false)
267
- # whether to validate the parsed statements and values
270
+ # @param [#to_s] base_uri (nil)
271
+ # the base URI to use when resolving relative URIs (not supported by
272
+ # all readers)
268
273
  # @param [Boolean] canonicalize (false)
269
274
  # whether to canonicalize parsed literals
275
+ # @param [Encoding] encoding (Encoding::UTF_8)
276
+ # the encoding of the input stream
270
277
  # @param [Boolean] intern (true)
271
278
  # whether to intern all parsed URIs
279
+ # @param [:PG, :SA] rdfstar (nil)
280
+ # support parsing RDF* statement resources.
281
+ # If `:PG`, referenced statements are also emitted.
282
+ # If `:SA`, referenced statements are not emitted.
272
283
  # @param [Hash] prefixes (Hash.new)
273
284
  # the prefix mappings to use (not supported by all readers)
274
- # @param [#to_s] base_uri (nil)
275
- # the base URI to use when resolving relative URIs (not supported by
276
- # all readers)
277
285
  # @param [Hash{Symbol => Object}] options
278
286
  # any additional options
287
+ # @param [Boolean] validate (false)
288
+ # whether to validate the parsed statements and values
279
289
  # @yield [reader] `self`
280
290
  # @yieldparam [RDF::Reader] reader
281
291
  # @yieldreturn [void] ignored
282
292
  def initialize(input = $stdin,
283
- encoding: Encoding::UTF_8,
284
- validate: false,
293
+ base_uri: nil,
285
294
  canonicalize: false,
295
+ encoding: Encoding::UTF_8,
286
296
  intern: true,
287
297
  prefixes: Hash.new,
288
- base_uri: nil,
298
+ rdfstar: nil,
299
+ validate: false,
289
300
  **options,
290
301
  &block)
291
302
 
292
303
  base_uri ||= input.base_uri if input.respond_to?(:base_uri)
293
304
  @options = options.merge({
294
- encoding: encoding,
295
- validate: validate,
305
+ base_uri: base_uri,
296
306
  canonicalize: canonicalize,
307
+ encoding: encoding,
297
308
  intern: intern,
298
309
  prefixes: prefixes,
299
- base_uri: base_uri
310
+ rdfstar: rdfstar,
311
+ validate: validate
300
312
  })
301
313
 
302
314
  @input = case input
@@ -389,6 +401,9 @@ module RDF
389
401
  # Statements are yielded in the order that they are read from the input
390
402
  # stream.
391
403
  #
404
+ # If the `rdfstar` option is `:PG` and triples include
405
+ # embedded statements, they are also enumerated.
406
+ #
392
407
  # @overload each_statement
393
408
  # @yield [statement]
394
409
  # each statement
@@ -405,7 +420,11 @@ module RDF
405
420
  def each_statement(&block)
406
421
  if block_given?
407
422
  begin
408
- loop { block.call(read_statement) }
423
+ loop do
424
+ st = read_statement
425
+ block.call(st)
426
+ each_pg_statement(st, &block) if options[:rdfstar] == :PG
427
+ end
409
428
  rescue EOFError
410
429
  rewind rescue nil
411
430
  end
@@ -422,6 +441,9 @@ module RDF
422
441
  # Triples are yielded in the order that they are read from the input
423
442
  # stream.
424
443
  #
444
+ # If the `rdfstar` option is `:PG` and triples include
445
+ # embedded statements, they are also enumerated.
446
+ #
425
447
  # @overload each_triple
426
448
  # @yield [subject, predicate, object]
427
449
  # each triple
@@ -439,7 +461,14 @@ module RDF
439
461
  def each_triple(&block)
440
462
  if block_given?
441
463
  begin
442
- loop { block.call(*read_triple) }
464
+ loop do
465
+ triple = read_triple
466
+ block.call(*triple)
467
+ if options[:rdfstar] == :PG
468
+ block.call(*triple[0].to_a) if triple[0].is_a?(Statement)
469
+ block.call(*triple[2].to_a) if triple[2].is_a?(Statement)
470
+ end
471
+ end
443
472
  rescue EOFError
444
473
  rewind rescue nil
445
474
  end
@@ -550,6 +579,22 @@ module RDF
550
579
  log_error("Expected object (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError)
551
580
  end
552
581
 
582
+ ##
583
+ # Recursively emit embedded statements in Property Graph mode
584
+ #
585
+ # @param [RDF::Statement] statement
586
+ def each_pg_statement(statement, &block)
587
+ if statement.subject.is_a?(Statement)
588
+ block.call(statement.subject)
589
+ each_pg_statement(statement.subject, &block)
590
+ end
591
+
592
+ if statement.object.is_a?(Statement)
593
+ block.call(statement.object)
594
+ each_pg_statement(statement.object, &block)
595
+ end
596
+ end
597
+
553
598
  public
554
599
  ##
555
600
  # Returns the encoding of the input stream.