rdf 3.0.13 → 3.1.4

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.
@@ -77,21 +77,23 @@ 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.
88
- def normalize!(hash_pattern = {}, options = {})
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.
89
+ def normalize!(*args)
90
+ hash_pattern = args.shift
91
+ options = args.shift || {}
92
+ anonymous_subject_format = options.fetch(:anonymous_subject_format, '__%s__')
89
93
  raise ArgumentError, "invalid hash pattern: #{hash_pattern.inspect}" unless hash_pattern.is_a?(Hash)
90
94
 
91
95
  counter = RDF::Query::HashPatternNormalizer::Counter.new
92
96
 
93
- anonymous_subject_format = (options[:anonymous_subject_format] || '__%s__').to_s
94
-
95
97
  hash_pattern.inject({}) { |acc, pair|
96
98
  subject, predicate_to_object = pair
97
99
 
@@ -184,7 +186,7 @@ module RDF; class Query
184
186
  # the query pattern as a hash.
185
187
  # @return [Hash{Symbol => Object}]
186
188
  # the resulting query pattern as a normalized hash.
187
- def normalize!(**hash_pattern)
189
+ def normalize!(hash_pattern)
188
190
  self.class.normalize!(hash_pattern, @options)
189
191
  end
190
192
  end # RDF::Query::HashPatternNormalizer
@@ -17,7 +17,7 @@ module RDF; class Query
17
17
  end
18
18
 
19
19
  ##
20
- # @overload initialize(**options)
20
+ # @overload initialize(options = {})
21
21
  # @param [Hash{Symbol => Object}] options
22
22
  # @option options [Variable, Resource, Symbol, nil] :subject (nil)
23
23
  # @option options [Variable, URI, Symbol, nil] :predicate (nil)
@@ -26,7 +26,7 @@ module RDF; class Query
26
26
  # A graph_name of nil matches any graph, a graph_name of false, matches only the default graph.
27
27
  # @option options [Boolean] :optional (false)
28
28
  #
29
- # @overload initialize(subject, predicate, object, **options)
29
+ # @overload initialize(subject, predicate, object, options = {})
30
30
  # @param [Variable, Resource, Symbol, nil] subject
31
31
  # @param [Variable, URI, Symbol, nil] predicate
32
32
  # @param [Variable, Termm, Symbol, nil] object
@@ -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
 
@@ -88,9 +88,15 @@ module RDF
88
88
  # @yieldreturn [String] another way to provide a sample, allows lazy for retrieving the sample.
89
89
  #
90
90
  # @return [Class]
91
- def self.for(options = {}, &block)
92
- options = options.merge(has_reader: true) if options.is_a?(Hash)
93
- if format = self.format || Format.for(options, &block)
91
+ def self.for(*arg, &block)
92
+ case arg.length
93
+ when 0 then arg = nil
94
+ when 1 then arg = arg.first
95
+ else
96
+ raise ArgumentError, "Format.for accepts zero or one argument, got #{arg.length}."
97
+ end
98
+ arg = arg.merge(has_reader: true) if arg.is_a?(Hash)
99
+ if format = self.format || Format.for(arg, &block)
94
100
  format.reader
95
101
  end
96
102
  end
@@ -115,6 +121,12 @@ module RDF
115
121
  # @return [Array<RDF::CLI::Option>]
116
122
  def self.options
117
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)},
118
130
  RDF::CLI::Option.new(
119
131
  symbol: :canonicalize,
120
132
  datatype: TrueClass,
@@ -147,11 +159,11 @@ module RDF
147
159
  end
148
160
  end,
149
161
  RDF::CLI::Option.new(
150
- symbol: :base_uri,
151
- control: :url,
152
- datatype: RDF::URI,
153
- on: ["--uri URI"],
154
- 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},
155
167
  RDF::CLI::Option.new(
156
168
  symbol: :validate,
157
169
  datatype: TrueClass,
@@ -206,7 +218,7 @@ module RDF
206
218
  headers['Accept'] ||= (self.format.accept_type + %w(*/*;q=0.1)).join(", ")
207
219
  end
208
220
 
209
- Util::File.open_file(filename, options) do |file|
221
+ Util::File.open_file(filename, **options) do |file|
210
222
  format_options = options.dup
211
223
  format_options[:content_type] ||= file.content_type if
212
224
  file.respond_to?(:content_type) &&
@@ -229,7 +241,7 @@ module RDF
229
241
  options[:filename] ||= filename
230
242
 
231
243
  if reader
232
- reader.new(file, options, &block)
244
+ reader.new(file, **options, &block)
233
245
  else
234
246
  raise FormatError, "unknown RDF format: #{format_options.inspect}#{"\nThis may be resolved with a require of the 'linkeddata' gem." unless Object.const_defined?(:LinkedData)}"
235
247
  end
@@ -255,42 +267,48 @@ module RDF
255
267
  #
256
268
  # @param [IO, File, String] input
257
269
  # the input stream to read
258
- # @param [Encoding] encoding (Encoding::UTF_8)
259
- # the encoding of the input stream
260
- # @param [Boolean] validate (false)
261
- # 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)
262
273
  # @param [Boolean] canonicalize (false)
263
274
  # whether to canonicalize parsed literals
275
+ # @param [Encoding] encoding (Encoding::UTF_8)
276
+ # the encoding of the input stream
264
277
  # @param [Boolean] intern (true)
265
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.
266
283
  # @param [Hash] prefixes (Hash.new)
267
284
  # the prefix mappings to use (not supported by all readers)
268
- # @param [#to_s] base_uri (nil)
269
- # the base URI to use when resolving relative URIs (not supported by
270
- # all readers)
271
285
  # @param [Hash{Symbol => Object}] options
272
286
  # any additional options
287
+ # @param [Boolean] validate (false)
288
+ # whether to validate the parsed statements and values
273
289
  # @yield [reader] `self`
274
290
  # @yieldparam [RDF::Reader] reader
275
291
  # @yieldreturn [void] ignored
276
292
  def initialize(input = $stdin,
277
- encoding: Encoding::UTF_8,
278
- validate: false,
293
+ base_uri: nil,
279
294
  canonicalize: false,
295
+ encoding: Encoding::UTF_8,
280
296
  intern: true,
281
297
  prefixes: Hash.new,
282
- base_uri: nil,
298
+ rdfstar: nil,
299
+ validate: false,
283
300
  **options,
284
301
  &block)
285
302
 
286
303
  base_uri ||= input.base_uri if input.respond_to?(:base_uri)
287
304
  @options = options.merge({
288
- encoding: encoding,
289
- validate: validate,
305
+ base_uri: base_uri,
290
306
  canonicalize: canonicalize,
307
+ encoding: encoding,
291
308
  intern: intern,
292
309
  prefixes: prefixes,
293
- base_uri: base_uri
310
+ rdfstar: rdfstar,
311
+ validate: validate
294
312
  })
295
313
 
296
314
  @input = case input
@@ -383,6 +401,9 @@ module RDF
383
401
  # Statements are yielded in the order that they are read from the input
384
402
  # stream.
385
403
  #
404
+ # If the `rdfstar` option is `:PG` and triples include
405
+ # embedded statements, they are also enumerated.
406
+ #
386
407
  # @overload each_statement
387
408
  # @yield [statement]
388
409
  # each statement
@@ -399,7 +420,11 @@ module RDF
399
420
  def each_statement(&block)
400
421
  if block_given?
401
422
  begin
402
- 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
403
428
  rescue EOFError
404
429
  rewind rescue nil
405
430
  end
@@ -416,6 +441,9 @@ module RDF
416
441
  # Triples are yielded in the order that they are read from the input
417
442
  # stream.
418
443
  #
444
+ # If the `rdfstar` option is `:PG` and triples include
445
+ # embedded statements, they are also enumerated.
446
+ #
419
447
  # @overload each_triple
420
448
  # @yield [subject, predicate, object]
421
449
  # each triple
@@ -433,7 +461,14 @@ module RDF
433
461
  def each_triple(&block)
434
462
  if block_given?
435
463
  begin
436
- 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
437
472
  rescue EOFError
438
473
  rewind rescue nil
439
474
  end
@@ -544,6 +579,22 @@ module RDF
544
579
  log_error("Expected object (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError)
545
580
  end
546
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
+
547
598
  public
548
599
  ##
549
600
  # Returns the encoding of the input stream.