rdf-n3 2.2.0 → 3.1.2

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.
Files changed (101) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +192 -69
  3. data/UNLICENSE +1 -1
  4. data/VERSION +1 -1
  5. data/lib/rdf/n3.rb +11 -8
  6. data/lib/rdf/n3/algebra.rb +204 -0
  7. data/lib/rdf/n3/algebra/builtin.rb +79 -0
  8. data/lib/rdf/n3/algebra/formula.rb +446 -0
  9. data/lib/rdf/n3/algebra/list/append.rb +42 -0
  10. data/lib/rdf/n3/algebra/list/first.rb +24 -0
  11. data/lib/rdf/n3/algebra/list/in.rb +48 -0
  12. data/lib/rdf/n3/algebra/list/last.rb +24 -0
  13. data/lib/rdf/n3/algebra/list/length.rb +24 -0
  14. data/lib/rdf/n3/algebra/list/member.rb +44 -0
  15. data/lib/rdf/n3/algebra/list_operator.rb +83 -0
  16. data/lib/rdf/n3/algebra/log/conclusion.rb +65 -0
  17. data/lib/rdf/n3/algebra/log/conjunction.rb +36 -0
  18. data/lib/rdf/n3/algebra/log/content.rb +34 -0
  19. data/lib/rdf/n3/algebra/log/equal_to.rb +34 -0
  20. data/lib/rdf/n3/algebra/log/implies.rb +102 -0
  21. data/lib/rdf/n3/algebra/log/includes.rb +70 -0
  22. data/lib/rdf/n3/algebra/log/n3_string.rb +34 -0
  23. data/lib/rdf/n3/algebra/log/not_equal_to.rb +23 -0
  24. data/lib/rdf/n3/algebra/log/not_includes.rb +27 -0
  25. data/lib/rdf/n3/algebra/log/output_string.rb +40 -0
  26. data/lib/rdf/n3/algebra/log/parsed_as_n3.rb +36 -0
  27. data/lib/rdf/n3/algebra/log/semantics.rb +40 -0
  28. data/lib/rdf/n3/algebra/math/absolute_value.rb +36 -0
  29. data/lib/rdf/n3/algebra/math/acos.rb +26 -0
  30. data/lib/rdf/n3/algebra/math/acosh.rb +26 -0
  31. data/lib/rdf/n3/algebra/math/asin.rb +26 -0
  32. data/lib/rdf/n3/algebra/math/asinh.rb +26 -0
  33. data/lib/rdf/n3/algebra/math/atan.rb +26 -0
  34. data/lib/rdf/n3/algebra/math/atanh.rb +26 -0
  35. data/lib/rdf/n3/algebra/math/ceiling.rb +28 -0
  36. data/lib/rdf/n3/algebra/math/cos.rb +40 -0
  37. data/lib/rdf/n3/algebra/math/cosh.rb +38 -0
  38. data/lib/rdf/n3/algebra/math/difference.rb +40 -0
  39. data/lib/rdf/n3/algebra/math/equal_to.rb +54 -0
  40. data/lib/rdf/n3/algebra/math/exponentiation.rb +35 -0
  41. data/lib/rdf/n3/algebra/math/floor.rb +28 -0
  42. data/lib/rdf/n3/algebra/math/greater_than.rb +41 -0
  43. data/lib/rdf/n3/algebra/math/less_than.rb +41 -0
  44. data/lib/rdf/n3/algebra/math/negation.rb +38 -0
  45. data/lib/rdf/n3/algebra/math/not_equal_to.rb +25 -0
  46. data/lib/rdf/n3/algebra/math/not_greater_than.rb +25 -0
  47. data/lib/rdf/n3/algebra/math/not_less_than.rb +25 -0
  48. data/lib/rdf/n3/algebra/math/product.rb +20 -0
  49. data/lib/rdf/n3/algebra/math/quotient.rb +36 -0
  50. data/lib/rdf/n3/algebra/math/remainder.rb +35 -0
  51. data/lib/rdf/n3/algebra/math/rounded.rb +26 -0
  52. data/lib/rdf/n3/algebra/math/sin.rb +40 -0
  53. data/lib/rdf/n3/algebra/math/sinh.rb +38 -0
  54. data/lib/rdf/n3/algebra/math/sum.rb +40 -0
  55. data/lib/rdf/n3/algebra/math/tan.rb +40 -0
  56. data/lib/rdf/n3/algebra/math/tanh.rb +38 -0
  57. data/lib/rdf/n3/algebra/not_implemented.rb +13 -0
  58. data/lib/rdf/n3/algebra/resource_operator.rb +123 -0
  59. data/lib/rdf/n3/algebra/str/concatenation.rb +27 -0
  60. data/lib/rdf/n3/algebra/str/contains.rb +33 -0
  61. data/lib/rdf/n3/algebra/str/contains_ignoring_case.rb +33 -0
  62. data/lib/rdf/n3/algebra/str/ends_with.rb +33 -0
  63. data/lib/rdf/n3/algebra/str/equal_ignoring_case.rb +34 -0
  64. data/lib/rdf/n3/algebra/str/format.rb +17 -0
  65. data/lib/rdf/n3/algebra/str/greater_than.rb +38 -0
  66. data/lib/rdf/n3/algebra/str/less_than.rb +33 -0
  67. data/lib/rdf/n3/algebra/str/matches.rb +37 -0
  68. data/lib/rdf/n3/algebra/str/not_equal_ignoring_case.rb +17 -0
  69. data/lib/rdf/n3/algebra/str/not_greater_than.rb +17 -0
  70. data/lib/rdf/n3/algebra/str/not_less_than.rb +17 -0
  71. data/lib/rdf/n3/algebra/str/not_matches.rb +18 -0
  72. data/lib/rdf/n3/algebra/str/replace.rb +35 -0
  73. data/lib/rdf/n3/algebra/str/scrape.rb +35 -0
  74. data/lib/rdf/n3/algebra/str/starts_with.rb +33 -0
  75. data/lib/rdf/n3/algebra/time/day.rb +35 -0
  76. data/lib/rdf/n3/algebra/time/day_of_week.rb +27 -0
  77. data/lib/rdf/n3/algebra/time/gm_time.rb +29 -0
  78. data/lib/rdf/n3/algebra/time/hour.rb +35 -0
  79. data/lib/rdf/n3/algebra/time/in_seconds.rb +59 -0
  80. data/lib/rdf/n3/algebra/time/local_time.rb +29 -0
  81. data/lib/rdf/n3/algebra/time/minute.rb +35 -0
  82. data/lib/rdf/n3/algebra/time/month.rb +35 -0
  83. data/lib/rdf/n3/algebra/time/second.rb +35 -0
  84. data/lib/rdf/n3/algebra/time/timezone.rb +36 -0
  85. data/lib/rdf/n3/algebra/time/year.rb +29 -0
  86. data/lib/rdf/n3/extensions.rb +221 -0
  87. data/lib/rdf/n3/format.rb +66 -1
  88. data/lib/rdf/n3/list.rb +630 -0
  89. data/lib/rdf/n3/reader.rb +834 -492
  90. data/lib/rdf/n3/reasoner.rb +282 -0
  91. data/lib/rdf/n3/refinements.rb +178 -0
  92. data/lib/rdf/n3/repository.rb +332 -0
  93. data/lib/rdf/n3/terminals.rb +80 -0
  94. data/lib/rdf/n3/vocab.rb +36 -3
  95. data/lib/rdf/n3/writer.rb +476 -239
  96. metadata +187 -68
  97. data/AUTHORS +0 -1
  98. data/History.markdown +0 -99
  99. data/lib/rdf/n3/patches/array_hacks.rb +0 -53
  100. data/lib/rdf/n3/reader/meta.rb +0 -641
  101. data/lib/rdf/n3/reader/parser.rb +0 -237
@@ -1,42 +1,39 @@
1
1
  # coding: utf-8
2
2
  module RDF::N3
3
3
  ##
4
- # A Turtle serialiser in Ruby
4
+ # A Notation-3 serialiser in Ruby
5
5
  #
6
6
  # Note that the natural interface is to write a whole graph at a time.
7
7
  # Writing statements or Triples will create a graph to add them to
8
8
  # and then serialize the graph.
9
9
  #
10
- # @example Obtaining a Turtle writer class
10
+ # @example Obtaining a N3 writer class
11
11
  # RDF::Writer.for(:n3) #=> RDF::N3::Writer
12
12
  # RDF::Writer.for("etc/test.n3")
13
- # RDF::Writer.for("etc/test.ttl")
14
13
  # RDF::Writer.for(file_name: "etc/test.n3")
15
- # RDF::Writer.for(file_name: "etc/test.ttl")
16
14
  # RDF::Writer.for(file_extension: "n3")
17
- # RDF::Writer.for(file_extension: "ttl")
18
15
  # RDF::Writer.for(content_type: "text/n3")
19
16
  #
20
- # @example Serializing RDF graph into an Turtle file
17
+ # @example Serializing RDF graph into an N3 file
21
18
  # RDF::N3::Writer.open("etc/test.n3") do |writer|
22
19
  # writer << graph
23
20
  # end
24
21
  #
25
- # @example Serializing RDF statements into an Turtle file
22
+ # @example Serializing RDF statements into an N3 file
26
23
  # RDF::N3::Writer.open("etc/test.n3") do |writer|
27
24
  # graph.each_statement do |statement|
28
25
  # writer << statement
29
26
  # end
30
27
  # end
31
28
  #
32
- # @example Serializing RDF statements into an Turtle string
29
+ # @example Serializing RDF statements into an N3 string
33
30
  # RDF::N3::Writer.buffer do |writer|
34
31
  # graph.each_statement do |statement|
35
32
  # writer << statement
36
33
  # end
37
34
  # end
38
35
  #
39
- # The writer will add prefix definitions, and use them for creating @prefix definitions, and minting QNames
36
+ # The writer will add prefix definitions, and use them for creating @prefix definitions, and minting pnames
40
37
  #
41
38
  # @example Creating @base and @prefix definitions in output
42
39
  # RDF::N3::Writer.buffer(base_uri: "http://example.com/", prefixes: {
@@ -52,12 +49,17 @@ module RDF::N3
52
49
  class Writer < RDF::Writer
53
50
  format RDF::N3::Format
54
51
  include RDF::Util::Logger
55
- QNAME = Meta::REGEXPS[:"http://www.w3.org/2000/10/swap/grammar/n3#qname"]
52
+ include Terminals
53
+ using Refinements
56
54
 
57
- # @return [Graph] Graph of statements serialized
55
+ # @return [RDF::Repository] Repository of statements serialized
56
+ attr_accessor :repo
57
+
58
+ # @return [RDF::Graph] Graph being serialized
58
59
  attr_accessor :graph
59
- # @return [URI] Base URI used for relativizing URIs
60
- attr_accessor :base_uri
60
+
61
+ # @return [Array<RDF::Node>] formulae names
62
+ attr_accessor :formula_names
61
63
 
62
64
  ##
63
65
  # N3 Writer options
@@ -78,7 +80,7 @@ module RDF::N3
78
80
  end
79
81
 
80
82
  ##
81
- # Initializes the Turtle writer instance.
83
+ # Initializes the N3 writer instance.
82
84
  #
83
85
  # @param [IO, File] output
84
86
  # the output stream
@@ -105,11 +107,15 @@ module RDF::N3
105
107
  # @yieldreturn [void]
106
108
  # @yield [writer]
107
109
  # @yieldparam [RDF::Writer] writer
108
- def initialize(output = $stdout, options = {}, &block)
110
+ def initialize(output = $stdout, **options, &block)
111
+ @repo = RDF::N3::Repository.new
112
+ @uri_to_pname = {}
113
+ @uri_to_prefix = {}
109
114
  super do
110
- @graph = RDF::Graph.new
111
- @uri_to_qname = {}
112
- @uri_to_prefix = {}
115
+ if base_uri
116
+ @uri_to_prefix[base_uri.to_s.end_with?('#', '/') ? base_uri : RDF::URI("#{base_uri}#")] = nil
117
+ end
118
+ reset
113
119
  if block_given?
114
120
  case block.arity
115
121
  when 0 then instance_eval(&block)
@@ -128,7 +134,19 @@ module RDF::N3
128
134
  # @raise [NotImplementedError] unless implemented in subclass
129
135
  # @abstract
130
136
  def write_triple(subject, predicate, object)
131
- @graph.insert(RDF::Statement(subject, predicate, object))
137
+ repo.insert(RDF::Statement(subject, predicate, object))
138
+ end
139
+
140
+ ##
141
+ # Adds a quad to be serialized
142
+ # @param [RDF::Resource] subject
143
+ # @param [RDF::URI] predicate
144
+ # @param [RDF::Value] object
145
+ # @param [RDF::Resource] graph_name
146
+ # @return [void]
147
+ def write_quad(subject, predicate, object, graph_name)
148
+ statement = RDF::Statement.new(subject, predicate, object, graph_name: graph_name)
149
+ repo.insert(statement)
132
150
  end
133
151
 
134
152
  ##
@@ -138,28 +156,51 @@ module RDF::N3
138
156
  # @see #write_triple
139
157
  def write_epilogue
140
158
  @max_depth = @options[:max_depth] || 3
141
- @base_uri = RDF::URI(@options[:base_uri])
142
159
 
143
160
  self.reset
144
161
 
145
- log_debug {"\nserialize: graph: #{@graph.size}"}
162
+ log_debug("\nserialize: repo:") {repo.size}
146
163
 
147
164
  preprocess
165
+
148
166
  start_document
149
167
 
150
- order_subjects.each do |subject|
151
- unless is_done?(subject)
152
- statement(subject)
168
+ @formula_names = repo.graph_names(unique: true)
169
+
170
+ with_graph(nil) do
171
+ count = 0
172
+ order_subjects.each do |subject|
173
+ unless is_done?(subject)
174
+ statement(subject, count)
175
+ count += 1
176
+ end
177
+ end
178
+
179
+ # Output any formulae not already serialized using owl:sameAs
180
+ formula_names.each do |graph_name|
181
+ next if graph_done?(graph_name)
182
+
183
+ # Add graph_name to @formulae
184
+ @formulae[graph_name] = true
185
+
186
+ log_debug {"formula(#{graph_name})"}
187
+ @output.write("\n#{indent}")
188
+ p_term(graph_name, :subject)
189
+ @output.write(" ")
190
+ predicate(RDF::OWL.sameAs)
191
+ @output.write(" ")
192
+ formula(graph_name, :graph_name)
193
+ @output.write(" .\n")
153
194
  end
154
195
  end
155
196
 
156
197
  super
157
198
  end
158
-
159
- # Return a QName for the URI, or nil. Adds namespace of QName to defined prefixes
199
+
200
+ # Return a pname for the URI, or nil. Adds namespace of pname to defined prefixes
160
201
  # @param [RDF::Resource] resource
161
202
  # @return [String, nil] value to use to identify URI
162
- def get_qname(resource)
203
+ def get_pname(resource)
163
204
  case resource
164
205
  when RDF::Node
165
206
  return options[:unique_bnodes] ? resource.to_unique_base : resource.to_base
@@ -169,56 +210,55 @@ module RDF::N3
169
210
  return nil
170
211
  end
171
212
 
172
- log_debug {"get_qname(#{resource}), std?}"}
173
- qname = case
174
- when @uri_to_qname.has_key?(uri)
175
- return @uri_to_qname[uri]
213
+ #log_debug {"get_pname(#{resource}), std?}"}
214
+ pname = case
215
+ when @uri_to_pname.key?(uri)
216
+ return @uri_to_pname[uri]
176
217
  when u = @uri_to_prefix.keys.detect {|u| uri.index(u.to_s) == 0}
177
218
  # Use a defined prefix
178
219
  prefix = @uri_to_prefix[u]
179
- prefix(prefix, u) unless u.to_s.empty? # Define for output
180
- log_debug {"get_qname: add prefix #{prefix.inspect} => #{u}"}
181
- uri.sub(u.to_s, "#{prefix}:")
220
+ unless u.to_s.empty?
221
+ prefix(prefix, u) unless u.to_s.empty?
222
+ #log_debug("get_pname") {"add prefix #{prefix.inspect} => #{u}"}
223
+ uri.sub(u.to_s, "#{prefix}:")
224
+ end
182
225
  when @options[:standard_prefixes] && vocab = RDF::Vocabulary.each.to_a.detect {|v| uri.index(v.to_uri.to_s) == 0}
183
226
  prefix = vocab.__name__.to_s.split('::').last.downcase
184
227
  @uri_to_prefix[vocab.to_uri.to_s] = prefix
185
228
  prefix(prefix, vocab.to_uri) # Define for output
186
- log_debug {"get_qname: add standard prefix #{prefix.inspect} => #{vocab.to_uri}"}
229
+ #log_debug {"get_pname: add standard prefix #{prefix.inspect} => #{vocab.to_uri}"}
187
230
  uri.sub(vocab.to_uri.to_s, "#{prefix}:")
188
231
  else
189
232
  nil
190
233
  end
191
-
192
- # Make sure qname is a valid qname
193
- if qname
194
- md = QNAME.match(qname)
195
- qname = nil unless md.to_s.length == qname.length
234
+
235
+ # Make sure pname is a valid pname
236
+ if pname
237
+ md = PNAME_LN.match(pname) || PNAME_NS.match(pname)
238
+ pname = nil unless md.to_s.length == pname.length
196
239
  end
197
240
 
198
- @uri_to_qname[uri] = qname
199
- rescue Addressable::URI::InvalidURIError => e
200
- raise RDF::WriterError, "Invalid URI #{resource.inspect}: #{e.message}"
241
+ @uri_to_pname[uri] = pname
201
242
  end
202
-
243
+
203
244
  # Take a hash from predicate uris to lists of values.
204
245
  # Sort the lists of values. Return a sorted list of properties.
205
- # @param [Hash{String => Array<Resource>}] properties A hash of Property to Resource mappings
206
- # @return [Array<String>}] Ordered list of properties. Uses predicate_order.
246
+ # @param [Hash{RDF::Term => Array<RDF::Term>}] properties A hash of Property to Resource mappings
247
+ # @return [Array<RDF::Term>}] Ordered list of properties. Uses predicate_order.
207
248
  def sort_properties(properties)
208
249
  # Make sorted list of properties
209
250
  prop_list = []
210
-
251
+
211
252
  predicate_order.each do |prop|
212
- next unless properties[prop.to_s]
213
- prop_list << prop.to_s
253
+ next unless properties.key?(prop)
254
+ prop_list << prop
214
255
  end
215
-
256
+
216
257
  properties.keys.sort.each do |prop|
217
- next if prop_list.include?(prop.to_s)
218
- prop_list << prop.to_s
258
+ next if prop_list.include?(prop)
259
+ prop_list << prop
219
260
  end
220
-
221
- log_debug {"sort_properties: #{prop_list.join(', ')}"}
261
+
222
262
  prop_list
223
263
  end
224
264
 
@@ -228,15 +268,19 @@ module RDF::N3
228
268
  # @param [RDF::Literal, String, #to_s] literal
229
269
  # @param [Hash{Symbol => Object}] options
230
270
  # @return [String]
231
- def format_literal(literal, options = {})
271
+ def format_literal(literal, **options)
232
272
  literal = literal.dup.canonicalize! if @options[:canonicalize]
233
273
  case literal
234
274
  when RDF::Literal
235
- case literal.datatype
275
+ case literal.valid? ? literal.datatype : false
236
276
  when RDF::XSD.boolean, RDF::XSD.integer, RDF::XSD.decimal
237
- literal.to_s
277
+ literal.canonicalize.to_s
238
278
  when RDF::XSD.double
239
- literal.to_s.sub('E', 'e') # Favor lower case exponent
279
+ if literal.nan? || literal.infinite?
280
+ quoted(literal.value) + "^^#{format_uri(literal.datatype)}"
281
+ else
282
+ literal.canonicalize.to_s
283
+ end
240
284
  else
241
285
  text = quoted(literal.value)
242
286
  text << "@#{literal.language}" if literal.has_language?
@@ -247,48 +291,59 @@ module RDF::N3
247
291
  quoted(literal.to_s)
248
292
  end
249
293
  end
250
-
294
+
251
295
  ##
252
- # Returns the Turtle/N3 representation of a URI reference.
296
+ # Returns the N3 representation of a URI reference.
253
297
  #
254
298
  # @param [RDF::URI] uri
255
299
  # @param [Hash{Symbol => Object}] options
256
300
  # @return [String]
257
- def format_uri(uri, options = {})
258
- md = relativize(uri)
259
- log_debug {"relativize(#{uri.inspect}) => #{md.inspect}"} if md != uri.to_s
260
- md != uri.to_s ? "<#{md}>" : (get_qname(uri) || "<#{uri}>")
301
+ def format_uri(uri, **options)
302
+ md = uri == base_uri ? '' : uri.relativize(base_uri)
303
+ log_debug("relativize") {"#{uri.to_sxp} => <#{md.inspect}>"} if md != uri.to_s
304
+ md != uri.to_s ? "<#{md}>" : (get_pname(uri) || "<#{uri}>")
261
305
  end
262
-
306
+
263
307
  ##
264
- # Returns the Turtle/N3 representation of a blank node.
308
+ # Returns the N3 representation of a blank node.
265
309
  #
266
310
  # @param [RDF::Node] node
267
311
  # @param [Hash{Symbol => Object}] options
268
312
  # @return [String]
269
- def format_node(node, options = {})
270
- options[:unique_bnodes] ? node.to_unique_base : node.to_base
313
+ def format_node(node, **options)
314
+ if node.id.match(/^([^_]+)_[^_]+_([^_]+)$/)
315
+ sn, seq = $1, $2.to_i
316
+ seq = nil if seq == 0
317
+ "_:#{sn}#{seq}"
318
+ elsif options[:unique_bnodes]
319
+ node.to_unique_base
320
+ else
321
+ node.to_base
322
+ end
271
323
  end
272
-
324
+
273
325
  protected
274
326
  # Output @base and @prefix definitions
275
327
  def start_document
276
- @started = true
277
-
278
- @output.write("#{indent}@base <#{base_uri}> .\n") unless base_uri.to_s.empty?
279
-
280
- log_debug {"start_document: #{prefixes.inspect}"}
328
+ @output.write("@base <#{base_uri}> .\n") unless base_uri.to_s.empty?
329
+
330
+ log_debug("start_document: prefixes") { prefixes.inspect}
281
331
  prefixes.keys.sort_by(&:to_s).each do |prefix|
282
- @output.write("#{indent}@prefix #{prefix}: <#{prefixes[prefix]}> .\n")
332
+ @output.write("@prefix #{prefix}: <#{prefixes[prefix]}> .\n")
333
+ end
334
+
335
+ # Universals and extentials at top-level
336
+ unless @universals.empty?
337
+ log_debug("start_document: universals") { @universals.inspect}
338
+ terms = @universals.map {|v| format_uri(RDF::URI(v.name.to_s))}
339
+ @output.write("@forAll #{terms.join(', ')} .\n")
340
+ end
341
+
342
+ unless @existentials.empty?
343
+ log_debug("start_document: existentials") { @existentials.inspect}
344
+ terms = @existentials.map {|v| format_uri(RDF::URI(v.name.to_s.sub(/_ext$/, '')))}
345
+ @output.write("@forSome #{terms.join(', ')} .\n")
283
346
  end
284
- end
285
-
286
- # If base_uri is defined, use it to try to make uri relative
287
- # @param [#to_s] uri
288
- # @return [String]
289
- def relativize(uri)
290
- uri = uri.to_s
291
- base_uri ? uri.sub(base_uri.to_s, "") : uri
292
347
  end
293
348
 
294
349
  # Defines rdf:type of subjects to be emitted at the beginning of the graph. Defaults to rdfs:Class
@@ -298,8 +353,18 @@ module RDF::N3
298
353
  # Defines order of predicates to to emit at begninning of a resource description. Defaults to
299
354
  # [rdf:type, rdfs:label, dc:title]
300
355
  # @return [Array<URI>]
301
- def predicate_order; [RDF.type, RDF::RDFS.label, RDF::URI("http://purl.org/dc/terms/title")]; end
302
-
356
+ def predicate_order
357
+ [
358
+ RDF.type,
359
+ RDF::RDFS.label,
360
+ RDF::RDFS.comment,
361
+ RDF::URI("http://purl.org/dc/terms/title"),
362
+ RDF::URI("http://purl.org/dc/terms/description"),
363
+ RDF::OWL.sameAs,
364
+ RDF::N3::Log.implies
365
+ ]
366
+ end
367
+
303
368
  # Order subjects for output. Override this to output subjects in another order.
304
369
  #
305
370
  # Uses #top_classes and #base_uri.
@@ -307,36 +372,46 @@ module RDF::N3
307
372
  def order_subjects
308
373
  seen = {}
309
374
  subjects = []
310
-
375
+
311
376
  # Start with base_uri
312
- if base_uri && @subjects.keys.include?(base_uri)
377
+ if base_uri && @subjects.keys.select(&:uri?).include?(base_uri)
313
378
  subjects << base_uri
314
379
  seen[base_uri] = true
315
380
  end
316
- log_debug {"subjects1: #{subjects.inspect}"}
317
-
381
+
318
382
  # Add distinguished classes
319
383
  top_classes.each do |class_uri|
320
- graph.query(predicate: RDF.type, object: class_uri).map {|st| st.subject}.sort.uniq.each do |subject|
321
- log_debug {"order_subjects: #{subject.inspect}"}
322
- subjects << subject
323
- seen[subject] = true
324
- end
384
+ graph.query({predicate: RDF.type, object: class_uri}).
385
+ map {|st| st.subject}.sort.uniq.each do |subject|
386
+ log_debug("order_subjects") {subject.to_sxp}
387
+ subjects << subject
388
+ seen[subject] = true
389
+ end
390
+ end
391
+
392
+ # Add formulae which are subjects in this graph
393
+ @formulae.each_key do |bn|
394
+ next unless @subjects.key?(bn)
395
+ subjects << bn
396
+ seen[bn] = true
397
+ end
398
+
399
+ # Mark as seen lists that are part of another list
400
+ @lists.values.flatten.each do |v|
401
+ seen[v] = true if @lists.key?(v)
325
402
  end
326
- log_debug {"subjects2: #{subjects.inspect}"}
327
-
403
+
404
+ list_elements = [] # Lists may be top-level elements
405
+
328
406
  # Sort subjects by resources over bnodes, ref_counts and the subject URI itself
329
- recursable = @subjects.keys.
407
+ recursable = (@subjects.keys - list_elements).
330
408
  select {|s| !seen.include?(s)}.
331
409
  map {|r| [r.node? ? 1 : 0, ref_count(r), r]}.
332
410
  sort
333
-
334
- log_debug {"subjects3: #{subjects.inspect}"}
411
+
335
412
  subjects += recursable.map{|r| r.last}
336
- log_debug {"subjects4: #{subjects.inspect}"}
337
- subjects
338
413
  end
339
-
414
+
340
415
  # Perform any preprocessing of statements required
341
416
  def preprocess
342
417
  # Load defined prefixes
@@ -347,50 +422,52 @@ module RDF::N3
347
422
 
348
423
  prefix(nil, @options[:default_namespace]) if @options[:default_namespace]
349
424
 
350
- @graph.each {|statement| preprocess_statement(statement)}
425
+ @options[:prefixes] = {} # Will define actual used when matched
426
+ repo.each {|statement| preprocess_statement(statement)}
427
+
428
+ vars = repo.enum_term.to_a.uniq.select {|r| r.is_a?(RDF::Query::Variable) && !r.to_s.end_with?('_quick')}
429
+ @universals = vars.reject(&:existential?)
430
+ @existentials = vars - @universals
351
431
  end
352
-
432
+
353
433
  # Perform any statement preprocessing required. This is used to perform reference counts and determine required
354
434
  # prefixes.
355
435
  # @param [Statement] statement
356
436
  def preprocess_statement(statement)
357
- #log_debug {"preprocess: #{statement.inspect}"}
358
- references = ref_count(statement.object) + 1
359
- @references[statement.object] = references
360
- @subjects[statement.subject] = true
361
-
362
- # Pre-fetch qnames, to fill prefixes
363
- get_qname(statement.subject)
364
- get_qname(statement.predicate)
365
- get_qname(statement.object)
366
- get_qname(statement.object.datatype) if statement.object.literal? && statement.object.datatype
367
-
368
- @references[statement.predicate] = ref_count(statement.predicate) + 1
369
- end
370
-
371
- # Return the number of times this node has been referenced in the object position
372
- # @return [Integer]
373
- def ref_count(node)
374
- @references.fetch(node, 0)
437
+ #log_debug("preprocess") {statement.inspect}
438
+
439
+ # Pre-fetch pnames, to fill prefixes
440
+ get_pname(statement.subject)
441
+ get_pname(statement.predicate)
442
+ get_pname(statement.object)
443
+ get_pname(statement.object.datatype) if statement.object.literal? && statement.object.datatype
444
+ end
445
+
446
+ # Perform graph-specific preprocessing
447
+ # @param [Statement] statement
448
+ def preprocess_graph_statement(statement)
449
+ bump_reference(statement.object)
450
+ # Count properties of this subject
451
+ @subjects[statement.subject] ||= {}
452
+ @subjects[statement.subject][statement.predicate] ||= 0
453
+ @subjects[statement.subject][statement.predicate] += 1
375
454
  end
376
455
 
377
456
  # Returns indent string multiplied by the depth
378
457
  # @param [Integer] modifier Increase depth by specified amount
379
458
  # @return [String] A number of spaces, depending on current depth
380
459
  def indent(modifier = 0)
381
- " " * (@depth + modifier)
460
+ " " * (@options.fetch(:log_depth, log_depth) * 2 + modifier)
382
461
  end
383
462
 
384
463
  # Reset internal helper instance variables
385
464
  def reset
386
- @depth = 0
465
+ @universals, @existentials = [], []
387
466
  @lists = {}
388
- @namespaces = {}
389
467
  @references = {}
390
468
  @serialized = {}
469
+ @graphs = {}
391
470
  @subjects = {}
392
- @shortNames = {}
393
- @started = false
394
471
  end
395
472
 
396
473
  ##
@@ -400,7 +477,7 @@ module RDF::N3
400
477
  # @return [String]
401
478
  def quoted(string)
402
479
  if string.to_s.match(/[\t\n\r]/)
403
- string = string.gsub('\\', '\\\\').gsub('"""', '\\"""')
480
+ string = string.gsub('\\', '\\\\\\\\').gsub('"""', '\\"\\"\\"')
404
481
  %("""#{string}""")
405
482
  else
406
483
  "\"#{escaped(string)}\""
@@ -408,153 +485,313 @@ module RDF::N3
408
485
  end
409
486
 
410
487
  private
411
-
488
+
412
489
  # Checks if l is a valid RDF list, i.e. no nodes have other properties.
413
- def is_valid_list(l)
414
- #log_debug {"is_valid_list: #{l.inspect}"}
415
- return (l.node? && RDF::List.new(subject: l, graph: @graph).valid?) || l == RDF.nil
416
- end
417
-
418
- def do_list(l)
419
- list = RDF::List.new(subject: l, graph: @graph)
420
- log_debug {"do_list: #{list.inspect}"}
421
- position = :subject
422
- list.each_statement do |st|
423
- next unless st.predicate == RDF.first
424
- log_debug {" list this: #{st.subject} first: #{st.object}[#{position}]"}
425
- path(st.object, position)
426
- subject_done(st.subject)
427
- position = :object
428
- end
429
- end
430
-
431
- def p_list(node, position)
432
- return false if !is_valid_list(node)
433
- #log_debug {"p_list: #{node.inspect}, #{position}"}
434
-
435
- @output.write(position == :subject ? "(" : " (")
436
- @depth += 2
437
- do_list(node)
438
- @depth -= 2
439
- @output.write(')')
490
+ def collection?(l)
491
+ return @lists.key?(l) || l.list?
440
492
  end
441
-
442
- def p_squared?(node, position)
443
- node.node? &&
444
- !@serialized.has_key?(node) &&
445
- ref_count(node) <= 1
446
- end
447
-
448
- def p_squared(node, position)
449
- return false unless p_squared?(node, position)
450
-
451
- #log_debug {"p_squared: #{node.inspect}, #{position}"}
452
- subject_done(node)
453
- @output.write(position == :subject ? '[' : ' [')
454
- @depth += 2
455
- predicate_list(node)
456
- @depth -= 2
457
- @output.write(']')
458
-
459
- true
493
+
494
+ def collection(node, position)
495
+ return false if !collection?(node)
496
+ log_debug("collection") do
497
+ "#{node.to_sxp}, " +
498
+ "pos: #{position}, " +
499
+ "rc: #{ref_count(node)}"
500
+ end
501
+
502
+ @output.write("(")
503
+ log_depth do
504
+ list = node.list? ? node : @lists[node]
505
+ log_debug("collection") {list.inspect}
506
+ subject_done(RDF.nil)
507
+ subject_done(node)
508
+ index = 0
509
+ list.each do |li|
510
+ log_debug("(list first)") {"#{li}[#{position}]"}
511
+ @output.write(" ") if index > 0
512
+ path(li, :object)
513
+ subject_done(li)
514
+ index += 1
515
+ end
516
+ end
517
+ @output.write(')')
460
518
  end
461
-
462
- def p_default(node, position)
463
- #log_debug {"p_default: #{node.inspect}, #{position}"}
464
- l = (position == :subject ? "" : " ") + format_term(node, options)
519
+
520
+ # Default singular resource representation.
521
+ def p_term(resource, position)
522
+ #log_debug("p_term") {"#{resource.to_sxp}, #{position}"}
523
+ l = if resource.is_a?(RDF::Query::Variable)
524
+ if resource.to_s.end_with?('_quick')
525
+ '?' + RDF::URI(resource.name).fragment.sub(/_quick$/, '')
526
+ else
527
+ format_term(RDF::URI(resource.name.to_s.sub(/_ext$/, '')))
528
+ end
529
+ elsif resource == RDF.nil
530
+ "()"
531
+ else
532
+ format_term(resource, **options)
533
+ end
465
534
  @output.write(l)
466
535
  end
467
-
468
- def path(node, position)
469
- log_debug do
470
- "path: #{node.inspect}, " +
536
+
537
+ # Represent a resource in subject, predicate or object position.
538
+ # Use either collection, blankNodePropertyList or singular resource notation.
539
+ def path(resource, position)
540
+ log_debug("path") do
541
+ "#{resource.to_sxp}, " +
471
542
  "pos: #{position}, " +
472
- "[]: #{is_valid_list(node)}, " +
473
- "p2?: #{p_squared?(node, position)}, " +
474
- "rc: #{ref_count(node)}"
543
+ "{}?: #{formula?(resource, position).inspect}, " +
544
+ "()?: #{collection?(resource).inspect}, " +
545
+ "[]?: #{blankNodePropertyList?(resource, position).inspect}, " +
546
+ "rc: #{ref_count(resource)}"
475
547
  end
476
- raise RDF::WriterError, "Cannot serialize node '#{node}'" unless p_list(node, position) || p_squared(node, position) || p_default(node, position)
548
+ raise RDF::WriterError, "Cannot serialize resource '#{resource}'" unless
549
+ formula(resource, position) ||
550
+ collection(resource, position) ||
551
+ blankNodePropertyList(resource, position) ||
552
+ p_term(resource, position)
477
553
  end
478
-
479
- def verb(node)
480
- log_debug {"verb: #{node.inspect}"}
481
- if node == RDF.type
482
- @output.write(" a")
554
+
555
+ def predicate(resource)
556
+ log_debug("predicate") {resource.to_sxp}
557
+ case resource
558
+ when RDF.type
559
+ @output.write("a")
560
+ when RDF::OWL.sameAs
561
+ @output.write("=")
562
+ when RDF::N3::Log.implies
563
+ @output.write("=>")
483
564
  else
484
- path(node, :predicate)
565
+ log_depth {path(resource, :predicate)}
485
566
  end
486
567
  end
487
-
488
- def object_list(objects)
489
- log_debug {"object_list: #{objects.inspect}"}
568
+
569
+ # Render an objectList having a common subject and predicate
570
+ def objectList(objects)
571
+ log_debug("objectList") {objects.inspect}
490
572
  return if objects.empty?
491
573
 
492
- objects.each_with_index do |obj, i|
493
- @output.write(",\n#{indent(4)}") if i > 0
494
- path(obj, :object)
574
+ log_depth do
575
+ objects.each_with_index do |obj, i|
576
+ if i > 0 && (formula?(obj, :object) || blankNodePropertyList?(obj, :object))
577
+ @output.write ", "
578
+ elsif i > 0
579
+ @output.write ",\n#{indent(4)}"
580
+ end
581
+ path(obj, :object)
582
+ end
495
583
  end
496
584
  end
497
-
498
- def predicate_list(subject)
585
+
586
+ # Render a predicateObjectList having a common subject.
587
+ # @return [Integer] the number of properties serialized
588
+ def predicateObjectList(subject, from_bpl = false)
499
589
  properties = {}
500
- @graph.query(subject: subject) do |st|
501
- properties[st.predicate.to_s] ||= []
502
- properties[st.predicate.to_s] << st.object
590
+ @graph.enum_statement.select {|s| s.subject.sameTerm?(subject)}.each do |st|
591
+ (properties[st.predicate] ||= []) << st.object
503
592
  end
504
593
 
505
- prop_list = sort_properties(properties) - [RDF.first.to_s, RDF.rest.to_s]
506
- log_debug {"predicate_list: #{prop_list.inspect}"}
507
- return if prop_list.empty?
594
+ prop_list = sort_properties(properties)
595
+ prop_list -= [RDF.first, RDF.rest] if @lists.key?(subject)
596
+ log_debug("predicateObjectList") { "subject: #{subject.to_sxp}, properties: #{prop_list.join(', ')}" }
597
+ return 0 if prop_list.empty?
508
598
 
509
- prop_list.each_with_index do |prop, i|
510
- begin
511
- @output.write(";\n#{indent(2)}") if i > 0
512
- verb(prop[0, 2] == "_:" ? RDF::Node.intern(prop.split(':').last) : RDF::URI.intern(prop))
513
- object_list(properties[prop])
514
- rescue Addressable::URI::InvalidURIError => e
515
- log_debug {"Predicate #{prop.inspect} is an invalid URI: #{e.message}"}
599
+ @output.write("\n#{indent(2)}") if properties.keys.length > 1 && from_bpl
600
+ log_depth do
601
+ prop_list.each_with_index do |prop, i|
602
+ begin
603
+ @output.write(";\n#{indent(2)}") if i > 0
604
+ predicate(prop)
605
+ @output.write(" ")
606
+ objectList(properties[prop])
607
+ end
516
608
  end
517
609
  end
610
+ properties.keys.length
611
+ end
612
+
613
+ # Can subject be represented as a blankNodePropertyList?
614
+ def blankNodePropertyList?(resource, position)
615
+ resource.node? &&
616
+ !formula?(resource, position) &&
617
+ !collection?(resource) &&
618
+ (!is_done?(resource) || position == :subject) &&
619
+ ref_count(resource) == (position == :object ? 1 : 0) &&
620
+ !repo.has_graph?(resource)
621
+ end
622
+
623
+ def blankNodePropertyList(resource, position)
624
+ return false unless blankNodePropertyList?(resource, position)
625
+
626
+ log_debug("blankNodePropertyList") {resource.to_sxp}
627
+ subject_done(resource)
628
+ @output.write((position == :subject ? "\n#{indent}[" : '['))
629
+ num_props = log_depth {predicateObjectList(resource, true)}
630
+ @output.write((num_props > 1 ? "\n#{indent(2)}" : "") + (position == :subject ? '] .' : ']'))
631
+ true
518
632
  end
519
-
520
- def s_squared?(subject)
521
- ref_count(subject) == 0 && subject.node? && !is_valid_list(subject)
522
- end
523
-
524
- def s_squared(subject)
525
- return false unless s_squared?(subject)
526
-
527
- log_debug {"s_squared: #{subject.inspect}"}
528
- @output.write("\n#{indent} [")
529
- @depth += 1
530
- predicate_list(subject)
531
- @depth -= 1
532
- @output.write("] .")
633
+
634
+ # Can subject be represented as a formula?
635
+ def formula?(resource, position)
636
+ !!@formulae[resource]
637
+ end
638
+
639
+ def formula(resource, position)
640
+ return false unless formula?(resource, position)
641
+
642
+ log_debug("formula") {resource.to_sxp}
643
+ subject_done(resource)
644
+ @output.write('{')
645
+ count = 0
646
+ log_depth do
647
+ with_graph(resource) do
648
+ order_subjects.each do |subject|
649
+ unless is_done?(subject)
650
+ statement(subject, count)
651
+ count += 1
652
+ end
653
+ end
654
+ end
655
+ end
656
+ @output.write((count > 0 ? "#{indent}" : "") + '}')
533
657
  true
534
658
  end
535
-
536
- def s_default(subject)
659
+
660
+ # Render triples having the same subject using an explicit subject
661
+ def triples(subject)
537
662
  @output.write("\n#{indent}")
538
663
  path(subject, :subject)
539
- predicate_list(subject)
540
- @output.write(" .")
664
+ @output.write(" ")
665
+ num_props = predicateObjectList(subject)
666
+ @output.puts("#{num_props > 0 ? ' ' : ''}.")
541
667
  true
542
668
  end
543
-
544
- def statement(subject)
545
- log_debug {"statement: #{subject.inspect}, s2?: #{s_squared?(subject)}"}
669
+
670
+ def statement(subject, count)
671
+ log_debug("statement") do
672
+ "#{subject.to_sxp}, " +
673
+ "{}?: #{formula?(subject, :subject).inspect}, " +
674
+ "()?: #{collection?(subject).inspect}, " +
675
+ "[]?: #{blankNodePropertyList?(subject, :subject).inspect}, "
676
+ end
546
677
  subject_done(subject)
547
- s_squared(subject) || s_default(subject)
548
- @output.write("\n")
678
+ blankNodePropertyList(subject, :subject) || triples(subject)
679
+ @output.puts if count > 0 || graph.graph_name
680
+ end
681
+
682
+ # Return the number of times this node has been referenced in the object position
683
+ # @return [Integer]
684
+ def ref_count(node)
685
+ @references.fetch(node, 0)
549
686
  end
550
-
687
+
688
+ # Increase the reference count of this resource
689
+ # @param [RDF::Resource] resource
690
+ # @return [Integer] resulting reference count
691
+ def bump_reference(resource)
692
+ @references[resource] = ref_count(resource) + 1
693
+ end
694
+
551
695
  def is_done?(subject)
552
696
  @serialized.include?(subject)
553
697
  end
554
-
698
+
555
699
  # Mark a subject as done.
556
700
  def subject_done(subject)
557
701
  @serialized[subject] = true
558
702
  end
703
+
704
+ def graph_done?(subject)
705
+ @graphs.include?(subject)
706
+ end
707
+
708
+ # Mark a graph as done.
709
+ def graph_done(graph_name)
710
+ @graphs[graph_name] = true
711
+ end
712
+
713
+ # Process a graph projection
714
+ def with_graph(graph_name)
715
+ old_lists, @lists = @lists, {}
716
+ old_references, @references = @references, {}
717
+ old_serialized, @serialized = @serialized, {}
718
+ old_subjects, @subjects = @subjects, {}
719
+ old_graph, @graph = @graph, repo.project_graph(graph_name)
720
+ old_formulae, @formulae = @formulae, {}
721
+
722
+ graph_done(graph_name)
723
+
724
+ lists = {}
725
+ graph.each do |statement|
726
+ preprocess_graph_statement(statement)
727
+ [statement.subject, statement.object].each do |resource|
728
+ @formulae[resource] = true if
729
+ resource.node? &&
730
+ (formula_names.include?(resource) || resource.id.start_with?('_form_'))
731
+
732
+ # First-class list may have members which are formulae, and need reference counts
733
+ if resource.list?
734
+ resource.each_descendant do |term|
735
+ bump_reference(term)
736
+ @formulae[term] = true if
737
+ term.node? &&
738
+ (formula_names.include?(term) || term.id.start_with?('_form_'))
739
+ end
740
+ end
741
+ end
742
+
743
+ # Collect list elements
744
+ if [RDF.first, RDF.rest].include?(statement.predicate) && statement.subject.node?
745
+ lists[statement.subject] ||= {}
746
+ lists[statement.subject][statement.predicate] = statement.object
747
+ end
748
+ end
749
+
750
+ # Remove list entries after head with more than two properties (other than rdf:type)
751
+ rests = lists.values.map {|props| props[RDF.rest]}
752
+
753
+ # Remove non-head lists that have too many properties
754
+ rests.select do |bn|
755
+ pc = 0
756
+ @subjects.fetch(bn, {}).each do |pred, count|
757
+ next if pred == RDF.type
758
+ pc += count
759
+ end
760
+ lists.delete(bn) if pc > 2
761
+ end
762
+
763
+ # Values for this list element, recursive
764
+ def list_values(bn, lists)
765
+ raise "no list" unless lists.has_key?(bn)
766
+ first, rest = lists[bn][RDF.first], lists[bn][RDF.rest]
767
+ (rest == RDF.nil ? [] : list_values(rest, lists)).unshift(first)
768
+ rescue
769
+ lists.delete(bn)
770
+ raise $!
771
+ end
772
+
773
+ # Create value arrays for each entry
774
+ lists.each do |bn, props|
775
+ begin
776
+ @lists[bn] = list_values(bn, lists)
777
+ rescue
778
+ # Skip this list element, if it raises an exception
779
+ lists.delete(bn)
780
+ end
781
+ end
782
+
783
+ # Mark all remaining rests done
784
+ rests.each {|bn| subject_done(bn) if lists.include?(bn)}
785
+
786
+ # Remove entries that are referenced as rdf:rest of some entry
787
+ lists.each do |bn, props|
788
+ @lists.delete(props[RDF.rest])
789
+ end
790
+
791
+ # Record nodes in subject or object
792
+ yield
793
+ ensure
794
+ @graph, @lists, @references, @serialized, @subjects, @formulae = old_graph, old_lists, old_references, old_serialized, old_subjects, old_formulae
795
+ end
559
796
  end
560
797
  end