iq_rdf 0.2.2 → 0.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9b8113dea4da94252393b4f5d662fb83e20dd201b82f8d350d51ea13cf616a6
4
- data.tar.gz: a3c5e7299288526e0e8c042b37cfb6b3d20a500e9e361b464fd6667e07ca94f8
3
+ metadata.gz: 86d24605dfac28de70ab6dc47bbd296adb42c4e855697b137a226a2a92e19f45
4
+ data.tar.gz: 6d23a834fffa345374df50eac0fbb8c0671a92799adff48ad077cad61df81677
5
5
  SHA512:
6
- metadata.gz: aef36d79a240d34c0f3f35ce6dc431f02bfc84e836a5b6050714c86cbac1c5a4e138f43be74f3169a8750b4a730989a0c26fbeb1e53cad9520e80330360b7dcd
7
- data.tar.gz: d6844c6e75ca177e5707eb8e87ce181a0a3232c754ad288cb086a2608f9feb13c45bcc2fe138be613c465b9c4f032bac50c10c8a543c82da16307a2bdee15c65
6
+ metadata.gz: e9816318bac2c2959053a30e6ec9c5ec531bc37089bb797d289887fc365524c83c02c460b93a25ba74a6287134ee4181d3002acab00627ec5a02d839b0494cb6
7
+ data.tar.gz: 5ca38d366409dea8d0cf6c3e0ee099a828223d00650c1068bb7986dc3087b3f77158037a0efcaf324ed232d00e8b53dc36f53feb8a0d28a0f4d1720345020774
data/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ ## [0.3.0] - 2026-09-10
4
+
5
+ * omit `rdf:type rdf:List` from RDF collections. The list structure is fully
6
+ defined by `rdf:first`/`rdf:rest`/`rdf:nil`, and Turtle's `( ... )` form
7
+ cannot express the type at all, so emitting it made the same data count
8
+ differently per format. **Documents containing collections now serialize to
9
+ fewer triples**, and N-Triples, Turtle and RDF/XML agree on the count. In
10
+ RDF/XML, list nodes are now `rdf:Description` rather than `rdf:List`.
11
+
12
+ ## [0.2.3] - 2026-09-10
13
+
14
+ * fix Turtle serialization of literals containing a carriage return. A long
15
+ string (`"""..."""`) was only used for newlines, so a lone carriage return
16
+ ended up raw inside a short string - invalid Turtle, which made parsers drop
17
+ the statement without warning. Quotes are now escaped inside long strings as
18
+ well, so a value ending in a quote or containing three of them no longer
19
+ breaks the output.
20
+ * serialize Turtle in linear rather than quadratic time. Building the output
21
+ with `+=` copied the whole string on every append, which made large documents
22
+ effectively impossible to serialize.
23
+ * serialize N-Triples faster by not counting a Hash with `Enumerable#count`,
24
+ which walks and allocates per entry. On a document with many blank nodes this
25
+ dominated the runtime.
26
+ * add `rdf-turtle` as a development dependency, so that tests can parse
27
+ generated output back instead of only comparing it to an expected string.
28
+
29
+ ## [0.2.2] - 2026-07-31
30
+
31
+ * fix N-Triples serialization of literals containing newlines, tabs or
32
+ carriage returns
33
+ * drop support for end-of-life rubies
34
+
35
+ ## [0.2.1]
36
+
37
+ * fix a frozen string literal warning
data/iq_rdf.gemspec CHANGED
@@ -25,15 +25,17 @@ Gem::Specification.new do |s|
25
25
  s.homepage = "http://github.com/innoq/iq_rdf"
26
26
  s.summary = "IqRdf - A builder like rdf library for ruby and rails"
27
27
  s.description = s.summary
28
- s.extra_rdoc_files = ['README.md', 'LICENSE']
28
+ s.extra_rdoc_files = ['README.md', 'CHANGELOG.md', 'LICENSE']
29
29
 
30
30
  s.add_dependency "bundler"
31
31
  s.add_dependency "builder"
32
32
  s.add_dependency "activesupport"
33
33
 
34
34
  s.add_development_dependency "minitest"
35
+ # parses generated output back, so that tests catch invalid serializations
36
+ s.add_development_dependency "rdf-turtle"
35
37
 
36
- s.files = %w(LICENSE README.md Rakefile iq_rdf.gemspec) + Dir.glob("{lib,rails,test}/**/*")
38
+ s.files = %w(LICENSE README.md CHANGELOG.md Rakefile iq_rdf.gemspec) + Dir.glob("{lib,rails,test}/**/*")
37
39
  s.test_files = Dir.glob("{test}/**/*")
38
40
  s.executables = Dir.glob("{bin}/**/*")
39
41
  s.require_paths = ["lib"]
@@ -33,7 +33,9 @@ module IqRdf
33
33
  elements ||= @elements.dup
34
34
  block.call({},
35
35
  lambda {
36
- xml.rdf :List do
36
+ # rdf:Description, not rdf:List: a typed element would assert
37
+ # rdf:type rdf:List, which Turtle's ( ... ) form cannot express
38
+ xml.rdf :Description do
37
39
  elements.shift.build_xml(xml) do |*args|
38
40
  xml.rdf(:first, *args)
39
41
  end
@@ -62,7 +62,9 @@ module IqRdf
62
62
  render_blank_node = lambda do |res|
63
63
  node_id = blank_nodes[res]
64
64
  unless node_id
65
- node_id = blank_nodes.count + 1
65
+ # size, not count: Hash does not override Enumerable#count, which
66
+ # walks every entry - once per blank node that is quadratic
67
+ node_id = blank_nodes.size + 1
66
68
  blank_nodes[res] = node_id
67
69
  end
68
70
  return "_:b#{node_id}"
@@ -75,7 +77,9 @@ module IqRdf
75
77
  sublist = list
76
78
  total = res.elements.length
77
79
  res.elements.each_with_index do |current_element, i|
78
- sublist::rdf.build_predicate("type", IqRdf::Rdf::build_uri("List")) # _:b* a rdf:List
80
+ # No a rdf:List here: rdf:first/rdf:rest/rdf:nil already define the
81
+ # collection, and Turtle's ( ... ) form cannot express the type at
82
+ # all - emitting it made the same data count differently per format.
79
83
  sublist::rdf.first(current_element) # _:b* rdf:first <...>
80
84
  last = i + 1 == total
81
85
  if last
@@ -143,18 +147,18 @@ module IqRdf
143
147
  end
144
148
 
145
149
  def to_turtle
146
- s = ""
150
+ s = +""
147
151
  @namespaces.values.sort{ |n1, n2| n1.turtle_token <=> n2.turtle_token }.each do |namespace|
148
- s += "@prefix #{namespace.turtle_token}: <#{namespace.uri_prefix}>.\n"
152
+ s << "@prefix #{namespace.turtle_token}: <#{namespace.uri_prefix}>.\n"
149
153
  end
150
- s += "\n"
154
+ s << "\n"
151
155
  @nodes.each do |subject|
152
156
  pref = subject.to_s(@document_language)
153
157
  indent = "".ljust(pref.length)
154
158
 
155
159
  # Render subject, if it is defined as a RDF-type.
156
160
  if subject.rdf_type
157
- s += "#{pref} a #{subject.rdf_type}"
161
+ s << "#{pref} a #{subject.rdf_type}"
158
162
  pref = ";\n" + indent
159
163
  end
160
164
 
@@ -163,13 +167,13 @@ module IqRdf
163
167
  objects = predicate.nodes.map { |object| object.to_s(indent: indent, lang: predicate.lang || subject.lang || @document_language) }
164
168
  .join(", ")
165
169
 
166
- s += "#{pref} #{predicate.to_s} #{objects}"
170
+ s << "#{pref} #{predicate.to_s} #{objects}"
167
171
 
168
172
  pref = ";\n" + indent
169
173
  end
170
- s += ".\n"
174
+ s << ".\n"
171
175
  if @config[:empty_line_between_triples]
172
- s += "\n"
176
+ s << "\n"
173
177
  end
174
178
  end
175
179
  s
@@ -15,7 +15,8 @@
15
15
  module IqRdf
16
16
  class Literal
17
17
 
18
- NTRIPLES_ESCAPE_MAP = {
18
+ # Turtle accepts the same escapes as N-Triples, so both formats share this
19
+ ESCAPE_MAP = {
19
20
  "\\" => "\\\\",
20
21
  '"' => '\\"',
21
22
  "\n" => "\\n",
@@ -23,6 +24,12 @@ module IqRdf
23
24
  "\t" => "\\t"
24
25
  }.freeze
25
26
 
27
+ # Inside """...""" line breaks and tabs are legal as they are
28
+ LONG_ESCAPE_MAP = {
29
+ "\\" => "\\\\",
30
+ '"' => '\\"'
31
+ }.freeze
32
+
26
33
  def initialize(obj, lang = nil, datatype = nil)
27
34
  raise "#{datatype.inspect} is not an URI" unless datatype.nil? || datatype.is_a?(::URI) || datatype.is_a?(IqRdf::Uri)
28
35
  @obj = obj
@@ -45,7 +52,6 @@ module IqRdf
45
52
  def to_s(options = {})
46
53
  lang = @lang || options[:lang] # Use the Literals lang when given
47
54
  lang = (lang && lang != :none) ? "@#{lang}" : ""
48
- quote = @obj.to_s.include?("\n") ? '"""' : '"'
49
55
  datatype = if @datatype.is_a?(::URI)
50
56
  "^^<#{@datatype.to_s}>"
51
57
  elsif @datatype.is_a?(IqRdf::Uri)
@@ -54,7 +60,17 @@ module IqRdf
54
60
  ""
55
61
  end
56
62
 
57
- "#{quote}#{@obj.to_s.gsub("\\", "\\\\\\\\").gsub(/"/, "\\\"")}#{quote}#{lang}#{datatype}"
63
+ value = @obj.to_s
64
+
65
+ if value.match?(/[\n\r]/)
66
+ # A long string keeps line breaks readable, which is the point of
67
+ # Turtle over N-Triples. Its grammar allows raw line breaks and tabs,
68
+ # so only backslashes and quotes need escaping - escaping every quote
69
+ # also rules out a run of three ending the string prematurely.
70
+ %("""#{value.gsub(/[\\"]/, LONG_ESCAPE_MAP)}"""#{lang}#{datatype})
71
+ else
72
+ %("#{value.gsub(/[\\"\n\r\t]/, ESCAPE_MAP)}"#{lang}#{datatype})
73
+ end
58
74
  end
59
75
 
60
76
  def to_ntriples(parent_lang = nil)
@@ -67,7 +83,7 @@ module IqRdf
67
83
  (lang && lang != :none) ? "@#{lang}" : ""
68
84
  end
69
85
 
70
- "\"#{@obj.to_s.gsub(/[\\"\n\r\t]/, NTRIPLES_ESCAPE_MAP)}\"#{suffix}"
86
+ "\"#{@obj.to_s.gsub(/[\\"\n\r\t]/, ESCAPE_MAP)}\"#{suffix}"
71
87
  end
72
88
 
73
89
  def build_xml(xml, &block)
@@ -13,5 +13,5 @@
13
13
  # limitations under the License.
14
14
 
15
15
  module IqRdf
16
- VERSION = "0.2.2"
16
+ VERSION = "0.3.0"
17
17
  end
@@ -101,13 +101,10 @@ _:b2 <http://www.test.de/title> "blubb" .
101
101
  document << IqRdf::testemann.testIt([IqRdf::hello, IqRdf::goodbye, "bla"])
102
102
 
103
103
  assert_equal(<<-rdf.strip, document.to_ntriples.strip)
104
- _:b2 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/1999/02/22-rdf-syntax-ns#List> .
105
104
  _:b2 <http://www.w3.org/1999/02/22-rdf-syntax-ns#first> <http://test.de/hello> .
106
105
  _:b2 <http://www.w3.org/1999/02/22-rdf-syntax-ns#rest> _:b3 .
107
- _:b3 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/1999/02/22-rdf-syntax-ns#List> .
108
106
  _:b3 <http://www.w3.org/1999/02/22-rdf-syntax-ns#first> <http://test.de/goodbye> .
109
107
  _:b3 <http://www.w3.org/1999/02/22-rdf-syntax-ns#rest> _:b4 .
110
- _:b4 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/1999/02/22-rdf-syntax-ns#List> .
111
108
  _:b4 <http://www.w3.org/1999/02/22-rdf-syntax-ns#first> "bla" .
112
109
  _:b4 <http://www.w3.org/1999/02/22-rdf-syntax-ns#rest> <http://www.w3.org/1999/02/22-rdf-syntax-ns#nil> .
113
110
  <http://test.de/testemann> <http://test.de/testIt> _:b2 .
data/test/test_helper.rb CHANGED
@@ -19,3 +19,5 @@ $LOAD_PATH << File.join(File.dirname(__FILE__), "../lib")
19
19
  require 'iq_rdf'
20
20
 
21
21
  require 'minitest/autorun'
22
+
23
+ require 'rdf/turtle'
data/test/turtle_test.rb CHANGED
@@ -163,6 +163,63 @@ over two lines"""@de;
163
163
  rdf
164
164
  end
165
165
 
166
+ # A carriage return without a newline used to end up raw inside a short
167
+ # string, which is invalid Turtle and made parsers drop the statement.
168
+ def test_control_characters_in_literals
169
+ document = IqRdf::Document.new('http://www.test.de/', :lang => :none)
170
+
171
+ document << IqRdf::testemann do |t|
172
+ t.carriage_return("first\rsecond")
173
+ t.tab("left\tright")
174
+ t.long_with_quote("first\nends with \"")
175
+ t.long_with_three_quotes("first\nwith \"\"\" inside")
176
+ t.long_with_backslash("first\nC:\\temp")
177
+ end
178
+
179
+ assert_equal(<<rdf, document.to_turtle)
180
+ @prefix : <http://www.test.de/>.
181
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
182
+
183
+ :testemann :carriage_return """first\rsecond""";
184
+ :tab "left\\tright";
185
+ :long_with_quote """first
186
+ ends with \\\"""";
187
+ :long_with_three_quotes """first
188
+ with \\\"\\\"\\\" inside""";
189
+ :long_with_backslash """first
190
+ C:\\\\temp""".
191
+ rdf
192
+ end
193
+
194
+ # The assertions above compare against an expected string, which cements the
195
+ # current output but says nothing about whether it is valid Turtle. Parsing
196
+ # the output back does: it catches escaping that a parser rejects, or that
197
+ # silently changes the value.
198
+ def test_literals_survive_a_round_trip
199
+ {
200
+ "plain" => "just text",
201
+ "newline" => "first\nsecond",
202
+ "carriage return" => "first\rsecond",
203
+ "carriage return and newline" => "first\r\nsecond",
204
+ "tab" => "left\tright",
205
+ "quote" => 'said "hello"',
206
+ "trailing quote" => 'ends with "',
207
+ "three quotes" => 'a """ b',
208
+ "backslash" => 'C:\\temp',
209
+ "newline and quote" => "first\nends with \"",
210
+ "newline and three quotes" => "first\nwith \"\"\" inside",
211
+ "newline and backslash" => "first\nC:\\temp",
212
+ }.each do |name, value|
213
+ document = IqRdf::Document.new('http://www.test.de/', :lang => :none)
214
+ document << IqRdf::testemann.some_predicate(value)
215
+
216
+ statements = RDF::Turtle::Reader.new(document.to_turtle, validate: true).to_a
217
+ assert_equal 1, statements.size, "expected one statement for #{name}"
218
+ assert_equal value, statements.first.object.value,
219
+ "#{name} did not survive the round trip"
220
+ end
221
+ end
222
+
166
223
  def test_supress_if_empty_otpion
167
224
  document = IqRdf::Document.new('http://www.test.de/')
168
225
  document.namespaces :foaf => 'http://xmlns.com/foaf/0.1/'
data/test/xml_test.rb CHANGED
@@ -103,15 +103,15 @@ rdf
103
103
  </rdf:Description>
104
104
  <rdf:Description rdf:about="http://www.umweltprobenbank.de/testemann">
105
105
  <testIt>
106
- <rdf:List>
106
+ <rdf:Description>
107
107
  <rdf:first rdf:resource="http://www.umweltprobenbank.de/hello"/>
108
108
  <rdf:rest>
109
- <rdf:List>
109
+ <rdf:Description>
110
110
  <rdf:first>bla</rdf:first>
111
111
  <rdf:rest rdf:resource="http://www.w3.org/1999/02/22-rdf-syntax-ns#nil"/>
112
- </rdf:List>
112
+ </rdf:Description>
113
113
  </rdf:rest>
114
- </rdf:List>
114
+ </rdf:Description>
115
115
  </testIt>
116
116
  </rdf:Description>
117
117
  <rdf:Description rdf:about="http://www.upb.de/u1023">
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iq_rdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Till Schulte-Coerne
@@ -65,15 +65,31 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rdf-turtle
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
68
82
  description: IqRdf - A builder like rdf library for ruby and rails
69
83
  email:
70
84
  - till.schulte-coerne@innoq.com
71
85
  executables: []
72
86
  extensions: []
73
87
  extra_rdoc_files:
88
+ - CHANGELOG.md
74
89
  - LICENSE
75
90
  - README.md
76
91
  files:
92
+ - CHANGELOG.md
77
93
  - LICENSE
78
94
  - README.md
79
95
  - Rakefile