rxerces 0.2.0 → 0.4.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGES.md +20 -0
- data/README.md +26 -2
- data/examples/schema_example.rb +107 -0
- data/ext/rxerces/extconf.rb +42 -0
- data/ext/rxerces/rxerces.cpp +834 -7
- data/lib/rxerces/nokogiri.rb +1 -0
- data/lib/rxerces/version.rb +1 -1
- data/rxerces.gemspec +5 -2
- data/spec/document_spec.rb +54 -0
- data/spec/node_spec.rb +448 -0
- data/spec/nokogiri_compatibility_spec.rb +69 -0
- data/spec/rxerces_shared.rb +1 -1
- data/spec/schema_spec.rb +76 -0
- data/spec/xpath_spec.rb +252 -18
- data.tar.gz.sig +0 -0
- metadata +7 -3
- metadata.gz.sig +0 -0
data/lib/rxerces/nokogiri.rb
CHANGED
data/lib/rxerces/version.rb
CHANGED
data/rxerces.gemspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Gem::Specification.new do |spec|
|
|
2
2
|
spec.name = "rxerces"
|
|
3
|
-
spec.version = "0.
|
|
3
|
+
spec.version = "0.4.0"
|
|
4
4
|
spec.author = "Daniel J. Berger"
|
|
5
5
|
spec.email = "djberg96@gmail.com"
|
|
6
6
|
spec.cert_chain = ["certs/djberg96_pub.pem"]
|
|
@@ -17,7 +17,10 @@ Gem::Specification.new do |spec|
|
|
|
17
17
|
spec.add_development_dependency "rake-compiler", "~> 1.2"
|
|
18
18
|
spec.add_development_dependency "rspec", "~> 3.12"
|
|
19
19
|
|
|
20
|
-
spec.description =
|
|
20
|
+
spec.description = <<-EOF
|
|
21
|
+
A Ruby XML library with Nokogiri-compatible API, powered by Xerces-C
|
|
22
|
+
instead of libxml2. It also optionally uses Xalan for Xpath 1.0 compliance.
|
|
23
|
+
EOF
|
|
21
24
|
|
|
22
25
|
spec.metadata = {
|
|
23
26
|
'homepage_uri' => 'https://github.com/djberg96/rxerces',
|
data/spec/document_spec.rb
CHANGED
|
@@ -63,4 +63,58 @@ RSpec.describe RXerces::XML::Document do
|
|
|
63
63
|
expect(result).to be_a(RXerces::XML::NodeSet)
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
|
+
|
|
67
|
+
describe "#encoding" do
|
|
68
|
+
it "returns UTF-8 for documents without explicit encoding" do
|
|
69
|
+
doc = RXerces::XML::Document.parse(simple_xml)
|
|
70
|
+
expect(doc.encoding).to eq('UTF-8')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "returns the encoding specified in the XML declaration" do
|
|
74
|
+
xml_with_encoding = '<?xml version="1.0" encoding="ISO-8859-1"?><root><item>Test</item></root>'
|
|
75
|
+
doc = RXerces::XML::Document.parse(xml_with_encoding)
|
|
76
|
+
expect(doc.encoding).to eq('ISO-8859-1')
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "returns the encoding for UTF-16 documents" do
|
|
80
|
+
xml_with_encoding = '<?xml version="1.0" encoding="UTF-16"?><root><item>Test</item></root>'
|
|
81
|
+
doc = RXerces::XML::Document.parse(xml_with_encoding)
|
|
82
|
+
expect(doc.encoding).to eq('UTF-16')
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe "#create_element" do
|
|
87
|
+
let(:doc) { RXerces::XML::Document.parse(simple_xml) }
|
|
88
|
+
|
|
89
|
+
it "creates a new element with the specified name" do
|
|
90
|
+
element = doc.create_element('book')
|
|
91
|
+
expect(element).to be_a(RXerces::XML::Element)
|
|
92
|
+
expect(element.name).to eq('book')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "creates an element that can have attributes set" do
|
|
96
|
+
element = doc.create_element('book')
|
|
97
|
+
attributes = element.attributes
|
|
98
|
+
expect(attributes).to be_a(Hash)
|
|
99
|
+
expect(attributes).to be_empty
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "creates an element that can have children added" do
|
|
103
|
+
element = doc.create_element('book')
|
|
104
|
+
element.add_child('Test Content')
|
|
105
|
+
expect(element.text).to eq('Test Content')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "creates an element that can be added to the document" do
|
|
109
|
+
root = doc.root
|
|
110
|
+
new_element = doc.create_element('new_child')
|
|
111
|
+
new_element.add_child('New content')
|
|
112
|
+
root.add_child(new_element)
|
|
113
|
+
|
|
114
|
+
# Verify the new element is in the document
|
|
115
|
+
result = doc.xpath('//new_child')
|
|
116
|
+
expect(result.length).to eq(1)
|
|
117
|
+
expect(result.first.text).to eq('New content')
|
|
118
|
+
end
|
|
119
|
+
end
|
|
66
120
|
end
|
data/spec/node_spec.rb
CHANGED
|
@@ -29,6 +29,32 @@ RSpec.describe RXerces::XML::Node do
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
describe "#namespace" do
|
|
33
|
+
let(:ns_xml) do
|
|
34
|
+
<<-XML
|
|
35
|
+
<root xmlns="http://example.com/default">
|
|
36
|
+
<item>Default namespace</item>
|
|
37
|
+
</root>
|
|
38
|
+
XML
|
|
39
|
+
end
|
|
40
|
+
let(:ns_doc) { RXerces::XML::Document.parse(ns_xml) }
|
|
41
|
+
|
|
42
|
+
it "returns nil for nodes without a namespace" do
|
|
43
|
+
expect(root.namespace).to be_nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "returns the default namespace URI" do
|
|
47
|
+
ns_root = ns_doc.root
|
|
48
|
+
expect(ns_root.namespace).to eq('http://example.com/default')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "returns the namespace for child elements inheriting default namespace" do
|
|
52
|
+
ns_root = ns_doc.root
|
|
53
|
+
item = ns_root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
54
|
+
expect(item.namespace).to eq('http://example.com/default')
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
32
58
|
describe "#text" do
|
|
33
59
|
it "returns text content" do
|
|
34
60
|
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
@@ -102,10 +128,432 @@ RSpec.describe RXerces::XML::Node do
|
|
|
102
128
|
end
|
|
103
129
|
end
|
|
104
130
|
|
|
131
|
+
describe "#parent" do
|
|
132
|
+
it "returns the parent node" do
|
|
133
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
134
|
+
parent = person.parent
|
|
135
|
+
expect(parent).to be_a(RXerces::XML::Element)
|
|
136
|
+
expect(parent.name).to eq('root')
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "returns the parent for nested elements" do
|
|
140
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
141
|
+
age = person.children.find { |n| n.name == 'age' }
|
|
142
|
+
parent = age.parent
|
|
143
|
+
expect(parent).to be_a(RXerces::XML::Element)
|
|
144
|
+
expect(parent.name).to eq('person')
|
|
145
|
+
expect(parent['id']).to eq('1')
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "returns the document for root element" do
|
|
149
|
+
parent = root.parent
|
|
150
|
+
expect(parent).not_to be_nil
|
|
151
|
+
expect(parent.name).to eq('#document')
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "returns nil for nodes without parent" do
|
|
155
|
+
# This is edge case - all nodes in a parsed document have parents
|
|
156
|
+
# but we test the safety of the method
|
|
157
|
+
expect(root.parent).not_to be_nil
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
describe "#attributes" do
|
|
162
|
+
it "returns a hash of attributes" do
|
|
163
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
164
|
+
attrs = person.attributes
|
|
165
|
+
expect(attrs).to be_a(Hash)
|
|
166
|
+
expect(attrs['id']).to eq('1')
|
|
167
|
+
expect(attrs['name']).to eq('Alice')
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it "returns all attributes" do
|
|
171
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
172
|
+
attrs = person.attributes
|
|
173
|
+
expect(attrs.keys).to match_array(['id', 'name'])
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it "returns empty hash for elements without attributes" do
|
|
177
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
178
|
+
age = person.children.find { |n| n.name == 'age' }
|
|
179
|
+
attrs = age.attributes
|
|
180
|
+
expect(attrs).to be_a(Hash)
|
|
181
|
+
expect(attrs).to be_empty
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "returns empty hash for text nodes" do
|
|
185
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
186
|
+
text_node = person.children.find { |n| n.is_a?(RXerces::XML::Text) }
|
|
187
|
+
attrs = text_node.attributes
|
|
188
|
+
expect(attrs).to be_a(Hash)
|
|
189
|
+
expect(attrs).to be_empty
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
describe "#next_sibling" do
|
|
194
|
+
it "returns the next sibling node" do
|
|
195
|
+
people = root.children.select { |n| n.is_a?(RXerces::XML::Element) }
|
|
196
|
+
first_person = people[0]
|
|
197
|
+
next_node = first_person.next_sibling
|
|
198
|
+
|
|
199
|
+
# May be a text node (whitespace) between elements
|
|
200
|
+
# Skip to next element if needed
|
|
201
|
+
while next_node && next_node.is_a?(RXerces::XML::Text)
|
|
202
|
+
next_node = next_node.next_sibling
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
expect(next_node).to be_a(RXerces::XML::Element)
|
|
206
|
+
expect(next_node.name).to eq('person')
|
|
207
|
+
expect(next_node['id']).to eq('2')
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it "returns nil for last sibling" do
|
|
211
|
+
people = root.children.select { |n| n.is_a?(RXerces::XML::Element) }
|
|
212
|
+
last_person = people.last
|
|
213
|
+
|
|
214
|
+
# Navigate past any trailing whitespace
|
|
215
|
+
next_node = last_person.next_sibling
|
|
216
|
+
while next_node && next_node.is_a?(RXerces::XML::Text)
|
|
217
|
+
next_node = next_node.next_sibling
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
expect(next_node).to be_nil
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it "can navigate through all siblings" do
|
|
224
|
+
first_element = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
225
|
+
siblings = []
|
|
226
|
+
current = first_element
|
|
227
|
+
|
|
228
|
+
while current
|
|
229
|
+
siblings << current if current.is_a?(RXerces::XML::Element)
|
|
230
|
+
current = current.next_sibling
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
expect(siblings.length).to eq(2)
|
|
234
|
+
expect(siblings[0]['id']).to eq('1')
|
|
235
|
+
expect(siblings[1]['id']).to eq('2')
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
describe "#previous_sibling" do
|
|
240
|
+
it "returns the previous sibling node" do
|
|
241
|
+
people = root.children.select { |n| n.is_a?(RXerces::XML::Element) }
|
|
242
|
+
second_person = people[1]
|
|
243
|
+
prev_node = second_person.previous_sibling
|
|
244
|
+
|
|
245
|
+
# May be a text node (whitespace) between elements
|
|
246
|
+
# Skip to previous element if needed
|
|
247
|
+
while prev_node && prev_node.is_a?(RXerces::XML::Text)
|
|
248
|
+
prev_node = prev_node.previous_sibling
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
expect(prev_node).to be_a(RXerces::XML::Element)
|
|
252
|
+
expect(prev_node.name).to eq('person')
|
|
253
|
+
expect(prev_node['id']).to eq('1')
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
it "returns nil for first sibling" do
|
|
257
|
+
first_element = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
258
|
+
|
|
259
|
+
# Navigate past any leading whitespace
|
|
260
|
+
prev_node = first_element.previous_sibling
|
|
261
|
+
while prev_node && prev_node.is_a?(RXerces::XML::Text)
|
|
262
|
+
prev_node = prev_node.previous_sibling
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
expect(prev_node).to be_nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it "can navigate backward through all siblings" do
|
|
269
|
+
last_element = root.children.select { |n| n.is_a?(RXerces::XML::Element) }.last
|
|
270
|
+
siblings = []
|
|
271
|
+
current = last_element
|
|
272
|
+
|
|
273
|
+
while current
|
|
274
|
+
siblings.unshift(current) if current.is_a?(RXerces::XML::Element)
|
|
275
|
+
current = current.previous_sibling
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
expect(siblings.length).to eq(2)
|
|
279
|
+
expect(siblings[0]['id']).to eq('1')
|
|
280
|
+
expect(siblings[1]['id']).to eq('2')
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
describe "#add_child" do
|
|
285
|
+
it "adds a text node from a string" do
|
|
286
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
287
|
+
initial_count = person.children.length
|
|
288
|
+
|
|
289
|
+
person.add_child("New text content")
|
|
290
|
+
|
|
291
|
+
expect(person.children.length).to eq(initial_count + 1)
|
|
292
|
+
last_child = person.children.last
|
|
293
|
+
expect(last_child).to be_a(RXerces::XML::Text)
|
|
294
|
+
expect(last_child.text).to eq("New text content")
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
it "adds a new element to another element" do
|
|
298
|
+
# Create a simple document to test with
|
|
299
|
+
simple_xml = '<root><parent></parent></root>'
|
|
300
|
+
simple_doc = RXerces::XML::Document.parse(simple_xml)
|
|
301
|
+
parent = simple_doc.root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
302
|
+
|
|
303
|
+
# Add text child
|
|
304
|
+
parent.add_child("Hello World")
|
|
305
|
+
|
|
306
|
+
expect(parent.children.length).to be > 0
|
|
307
|
+
text_child = parent.children.find { |n| n.is_a?(RXerces::XML::Text) }
|
|
308
|
+
expect(text_child.text).to eq("Hello World")
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
it "allows building a document structure" do
|
|
312
|
+
simple_xml = '<root></root>'
|
|
313
|
+
simple_doc = RXerces::XML::Document.parse(simple_xml)
|
|
314
|
+
root = simple_doc.root
|
|
315
|
+
|
|
316
|
+
# Add multiple children
|
|
317
|
+
root.add_child("First text")
|
|
318
|
+
root.add_child("Second text")
|
|
319
|
+
|
|
320
|
+
text_nodes = root.children.select { |n| n.is_a?(RXerces::XML::Text) }
|
|
321
|
+
expect(text_nodes.length).to eq(2)
|
|
322
|
+
expect(text_nodes[0].text).to eq("First text")
|
|
323
|
+
expect(text_nodes[1].text).to eq("Second text")
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
it "modifies the document" do
|
|
327
|
+
simple_xml = '<item></item>'
|
|
328
|
+
simple_doc = RXerces::XML::Document.parse(simple_xml)
|
|
329
|
+
item = simple_doc.root
|
|
330
|
+
|
|
331
|
+
item.add_child("Content")
|
|
332
|
+
|
|
333
|
+
xml_output = simple_doc.to_s
|
|
334
|
+
expect(xml_output).to include("Content")
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
describe "#remove" do
|
|
339
|
+
it "removes a node from its parent" do
|
|
340
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
341
|
+
initial_count = root.children.select { |n| n.is_a?(RXerces::XML::Element) }.length
|
|
342
|
+
|
|
343
|
+
person.remove
|
|
344
|
+
|
|
345
|
+
remaining = root.children.select { |n| n.is_a?(RXerces::XML::Element) }
|
|
346
|
+
expect(remaining.length).to eq(initial_count - 1)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
it "removes a child element from parent" do
|
|
350
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
351
|
+
age = person.children.find { |n| n.name == 'age' }
|
|
352
|
+
initial_count = person.children.select { |n| n.is_a?(RXerces::XML::Element) }.length
|
|
353
|
+
|
|
354
|
+
age.remove
|
|
355
|
+
|
|
356
|
+
remaining = person.children.select { |n| n.is_a?(RXerces::XML::Element) }
|
|
357
|
+
expect(remaining.length).to eq(initial_count - 1)
|
|
358
|
+
expect(person.children.find { |n| n.name == 'age' }).to be_nil
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
it "modifies the document" do
|
|
362
|
+
simple_xml = '<root><item>Remove me</item><keep>Keep me</keep></root>'
|
|
363
|
+
simple_doc = RXerces::XML::Document.parse(simple_xml)
|
|
364
|
+
item = simple_doc.xpath('//item').first
|
|
365
|
+
|
|
366
|
+
item.remove
|
|
367
|
+
|
|
368
|
+
xml_output = simple_doc.to_s
|
|
369
|
+
expect(xml_output).not_to include("Remove me")
|
|
370
|
+
expect(xml_output).to include("Keep me")
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
it "raises error when node has no parent" do
|
|
374
|
+
# The root element's parent is the document, so this should work
|
|
375
|
+
# We'll test with a document node instead
|
|
376
|
+
expect {
|
|
377
|
+
root.parent.remove
|
|
378
|
+
}.to raise_error(RuntimeError, /no parent/)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
describe "#unlink" do
|
|
383
|
+
it "is an alias for remove" do
|
|
384
|
+
simple_xml = '<root><item>Test</item></root>'
|
|
385
|
+
simple_doc = RXerces::XML::Document.parse(simple_xml)
|
|
386
|
+
item = simple_doc.xpath('//item').first
|
|
387
|
+
|
|
388
|
+
expect(item).to respond_to(:unlink)
|
|
389
|
+
item.unlink
|
|
390
|
+
|
|
391
|
+
xml_output = simple_doc.to_s
|
|
392
|
+
expect(xml_output).not_to include("Test")
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
105
396
|
describe "#xpath" do
|
|
106
397
|
it "returns a NodeSet" do
|
|
107
398
|
result = root.xpath('.//age')
|
|
108
399
|
expect(result).to be_a(RXerces::XML::NodeSet)
|
|
109
400
|
end
|
|
110
401
|
end
|
|
402
|
+
|
|
403
|
+
describe "#inner_html" do
|
|
404
|
+
it "returns the XML content of children without parent tags" do
|
|
405
|
+
person = root.xpath('//person').first
|
|
406
|
+
inner = person.inner_html
|
|
407
|
+
expect(inner).to include('<age>')
|
|
408
|
+
expect(inner).to include('<city>')
|
|
409
|
+
expect(inner).not_to include('<person')
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
it "returns empty string for nodes without children" do
|
|
413
|
+
age = root.xpath('//age').first
|
|
414
|
+
inner = age.inner_html
|
|
415
|
+
expect(inner).to eq('30')
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
it "includes multiple children" do
|
|
419
|
+
person = root.xpath('//person').first
|
|
420
|
+
inner = person.inner_html
|
|
421
|
+
expect(inner).to include('<age>30</age>')
|
|
422
|
+
expect(inner).to include('<city>New York</city>')
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
describe "#inner_xml" do
|
|
427
|
+
it "is an alias for inner_html" do
|
|
428
|
+
person = root.xpath('//person').first
|
|
429
|
+
expect(person.inner_xml).to eq(person.inner_html)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
it "returns the same XML content" do
|
|
433
|
+
person = root.xpath('//person').first
|
|
434
|
+
inner = person.inner_xml
|
|
435
|
+
expect(inner).to include('<age>')
|
|
436
|
+
expect(inner).to include('<city>')
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
describe "#path" do
|
|
441
|
+
it "returns the XPath to the root element" do
|
|
442
|
+
expect(root.path).to eq('/root[1]')
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
it "returns the XPath to a nested element" do
|
|
446
|
+
person = root.xpath('//person').first
|
|
447
|
+
expect(person.path).to eq('/root[1]/person[1]')
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
it "returns the XPath to the second person element" do
|
|
451
|
+
people = root.xpath('//person')
|
|
452
|
+
second_person = people[1]
|
|
453
|
+
expect(second_person.path).to eq('/root[1]/person[2]')
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
it "returns the XPath to deeply nested elements" do
|
|
457
|
+
age = root.xpath('//age').first
|
|
458
|
+
expect(age.path).to eq('/root[1]/person[1]/age[1]')
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
describe "#blank?" do
|
|
463
|
+
let(:blank_xml) { '<root><empty></empty><whitespace> </whitespace><content>Hello</content></root>' }
|
|
464
|
+
let(:blank_doc) { RXerces::XML::Document.parse(blank_xml) }
|
|
465
|
+
|
|
466
|
+
it "returns false for elements with text content" do
|
|
467
|
+
content = blank_doc.xpath('//content').first
|
|
468
|
+
expect(content.blank?).to be false
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
it "returns true for empty elements" do
|
|
472
|
+
empty = blank_doc.xpath('//empty').first
|
|
473
|
+
expect(empty.blank?).to be true
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
it "returns true for elements with only whitespace" do
|
|
477
|
+
whitespace = blank_doc.xpath('//whitespace').first
|
|
478
|
+
expect(whitespace.blank?).to be true
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
it "returns false for elements with child elements" do
|
|
482
|
+
root = blank_doc.root
|
|
483
|
+
expect(root.blank?).to be false
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
it "returns false for text nodes with content" do
|
|
487
|
+
content = blank_doc.xpath('//content').first
|
|
488
|
+
text_node = content.children.first
|
|
489
|
+
expect(text_node.blank?).to be false
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
it "returns true for text nodes with only whitespace" do
|
|
493
|
+
whitespace = blank_doc.xpath('//whitespace').first
|
|
494
|
+
text_node = whitespace.children.first
|
|
495
|
+
expect(text_node.blank?).to be true
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
describe "#search" do
|
|
500
|
+
it "is an alias for xpath" do
|
|
501
|
+
result1 = root.search('.//age')
|
|
502
|
+
result2 = root.xpath('.//age')
|
|
503
|
+
expect(result1.length).to eq(result2.length)
|
|
504
|
+
expect(result1.first.text).to eq(result2.first.text)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
it "returns a NodeSet" do
|
|
508
|
+
result = root.search('.//person')
|
|
509
|
+
expect(result).to be_a(RXerces::XML::NodeSet)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
it "finds nested elements" do
|
|
513
|
+
result = root.search('.//age')
|
|
514
|
+
expect(result.length).to eq(2)
|
|
515
|
+
expect(result.first.text).to eq('30')
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
describe "#at_xpath" do
|
|
520
|
+
it "returns the first matching node" do
|
|
521
|
+
result = root.at_xpath('.//age')
|
|
522
|
+
expect(result).to be_a(RXerces::XML::Element)
|
|
523
|
+
expect(result.text).to eq('30')
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
it "returns nil when no match found" do
|
|
527
|
+
result = root.at_xpath('.//nonexistent')
|
|
528
|
+
expect(result).to be_nil
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
it "returns only the first match when multiple exist" do
|
|
532
|
+
result = root.at_xpath('.//person')
|
|
533
|
+
expect(result['id']).to eq('1')
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
describe "#at" do
|
|
538
|
+
it "is an alias for at_xpath" do
|
|
539
|
+
result1 = root.at('.//age')
|
|
540
|
+
result2 = root.at_xpath('.//age')
|
|
541
|
+
expect(result1.text).to eq(result2.text)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
it "returns the first matching element" do
|
|
545
|
+
result = root.at('.//city')
|
|
546
|
+
expect(result.text).to eq('New York')
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
describe "#css" do
|
|
551
|
+
it "raises NotImplementedError for CSS selectors" do
|
|
552
|
+
expect { root.css('div.class') }.to raise_error(NotImplementedError, /CSS selectors are not supported/)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
it "suggests using xpath instead" do
|
|
556
|
+
expect { root.css('p') }.to raise_error(NotImplementedError, /Use xpath/)
|
|
557
|
+
end
|
|
558
|
+
end
|
|
111
559
|
end
|
|
@@ -61,6 +61,12 @@ RSpec.describe "Nokogiri compatibility" do
|
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
describe "Nokogiri::XML::Schema" do
|
|
65
|
+
it "is an alias for RXerces::XML::Schema" do
|
|
66
|
+
expect(Nokogiri::XML::Schema).to eq(RXerces::XML::Schema)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
64
70
|
describe "API compatibility" do
|
|
65
71
|
let(:doc) { Nokogiri.XML(simple_xml) }
|
|
66
72
|
|
|
@@ -95,4 +101,67 @@ RSpec.describe "Nokogiri compatibility" do
|
|
|
95
101
|
expect(children).to be_an(Array)
|
|
96
102
|
end
|
|
97
103
|
end
|
|
104
|
+
|
|
105
|
+
describe "Schema validation compatibility" do
|
|
106
|
+
let(:xsd) do
|
|
107
|
+
<<~XSD
|
|
108
|
+
<?xml version="1.0"?>
|
|
109
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
110
|
+
<xs:element name="person">
|
|
111
|
+
<xs:complexType>
|
|
112
|
+
<xs:sequence>
|
|
113
|
+
<xs:element name="name" type="xs:string"/>
|
|
114
|
+
<xs:element name="age" type="xs:integer"/>
|
|
115
|
+
</xs:sequence>
|
|
116
|
+
</xs:complexType>
|
|
117
|
+
</xs:element>
|
|
118
|
+
</xs:schema>
|
|
119
|
+
XSD
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
let(:valid_xml) do
|
|
123
|
+
<<~XML
|
|
124
|
+
<?xml version="1.0"?>
|
|
125
|
+
<person>
|
|
126
|
+
<name>John</name>
|
|
127
|
+
<age>30</age>
|
|
128
|
+
</person>
|
|
129
|
+
XML
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
let(:invalid_xml) do
|
|
133
|
+
<<~XML
|
|
134
|
+
<?xml version="1.0"?>
|
|
135
|
+
<person>
|
|
136
|
+
<name>Jane</name>
|
|
137
|
+
<age>invalid</age>
|
|
138
|
+
</person>
|
|
139
|
+
XML
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "provides Schema.from_string method" do
|
|
143
|
+
schema = Nokogiri::XML::Schema.from_string(xsd)
|
|
144
|
+
expect(schema).to be_a(Nokogiri::XML::Schema)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "provides Schema.from_document method" do
|
|
148
|
+
doc = Nokogiri::XML.parse(xsd)
|
|
149
|
+
schema = Nokogiri::XML::Schema.from_document(doc)
|
|
150
|
+
expect(schema).to be_a(Nokogiri::XML::Schema)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "validates a valid document" do
|
|
154
|
+
schema = Nokogiri::XML::Schema.from_string(xsd)
|
|
155
|
+
doc = Nokogiri::XML.parse(valid_xml)
|
|
156
|
+
errors = doc.validate(schema)
|
|
157
|
+
expect(errors).to be_empty
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it "returns errors for an invalid document" do
|
|
161
|
+
schema = Nokogiri::XML::Schema.from_string(xsd)
|
|
162
|
+
doc = Nokogiri::XML.parse(invalid_xml)
|
|
163
|
+
errors = doc.validate(schema)
|
|
164
|
+
expect(errors).not_to be_empty
|
|
165
|
+
end
|
|
166
|
+
end
|
|
98
167
|
end
|
data/spec/rxerces_shared.rb
CHANGED