rxerces 0.2.0 → 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.
@@ -1,3 +1,3 @@
1
1
  module RXerces
2
- VERSION = "0.2.0".freeze
2
+ VERSION = "0.3.0".freeze
3
3
  end
data/rxerces.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "rxerces"
3
- spec.version = "0.2.0"
3
+ spec.version = "0.3.0"
4
4
  spec.author = "Daniel J. Berger"
5
5
  spec.email = "djberg96@gmail.com"
6
6
  spec.cert_chain = ["certs/djberg96_pub.pem"]
@@ -63,4 +63,39 @@ 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 "#create_element" do
68
+ let(:doc) { RXerces::XML::Document.parse(simple_xml) }
69
+
70
+ it "creates a new element with the specified name" do
71
+ element = doc.create_element('book')
72
+ expect(element).to be_a(RXerces::XML::Element)
73
+ expect(element.name).to eq('book')
74
+ end
75
+
76
+ it "creates an element that can have attributes set" do
77
+ element = doc.create_element('book')
78
+ attributes = element.attributes
79
+ expect(attributes).to be_a(Hash)
80
+ expect(attributes).to be_empty
81
+ end
82
+
83
+ it "creates an element that can have children added" do
84
+ element = doc.create_element('book')
85
+ element.add_child('Test Content')
86
+ expect(element.text).to eq('Test Content')
87
+ end
88
+
89
+ it "creates an element that can be added to the document" do
90
+ root = doc.root
91
+ new_element = doc.create_element('new_child')
92
+ new_element.add_child('New content')
93
+ root.add_child(new_element)
94
+
95
+ # Verify the new element is in the document
96
+ result = doc.xpath('//new_child')
97
+ expect(result.length).to eq(1)
98
+ expect(result.first.text).to eq('New content')
99
+ end
100
+ end
66
101
  end
data/spec/node_spec.rb CHANGED
@@ -102,10 +102,371 @@ RSpec.describe RXerces::XML::Node do
102
102
  end
103
103
  end
104
104
 
105
+ describe "#parent" do
106
+ it "returns the parent node" do
107
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
108
+ parent = person.parent
109
+ expect(parent).to be_a(RXerces::XML::Element)
110
+ expect(parent.name).to eq('root')
111
+ end
112
+
113
+ it "returns the parent for nested elements" do
114
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
115
+ age = person.children.find { |n| n.name == 'age' }
116
+ parent = age.parent
117
+ expect(parent).to be_a(RXerces::XML::Element)
118
+ expect(parent.name).to eq('person')
119
+ expect(parent['id']).to eq('1')
120
+ end
121
+
122
+ it "returns the document for root element" do
123
+ parent = root.parent
124
+ expect(parent).not_to be_nil
125
+ expect(parent.name).to eq('#document')
126
+ end
127
+
128
+ it "returns nil for nodes without parent" do
129
+ # This is edge case - all nodes in a parsed document have parents
130
+ # but we test the safety of the method
131
+ expect(root.parent).not_to be_nil
132
+ end
133
+ end
134
+
135
+ describe "#attributes" do
136
+ it "returns a hash of attributes" do
137
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
138
+ attrs = person.attributes
139
+ expect(attrs).to be_a(Hash)
140
+ expect(attrs['id']).to eq('1')
141
+ expect(attrs['name']).to eq('Alice')
142
+ end
143
+
144
+ it "returns all attributes" do
145
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
146
+ attrs = person.attributes
147
+ expect(attrs.keys).to match_array(['id', 'name'])
148
+ end
149
+
150
+ it "returns empty hash for elements without attributes" do
151
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
152
+ age = person.children.find { |n| n.name == 'age' }
153
+ attrs = age.attributes
154
+ expect(attrs).to be_a(Hash)
155
+ expect(attrs).to be_empty
156
+ end
157
+
158
+ it "returns empty hash for text nodes" do
159
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
160
+ text_node = person.children.find { |n| n.is_a?(RXerces::XML::Text) }
161
+ attrs = text_node.attributes
162
+ expect(attrs).to be_a(Hash)
163
+ expect(attrs).to be_empty
164
+ end
165
+ end
166
+
167
+ describe "#next_sibling" do
168
+ it "returns the next sibling node" do
169
+ people = root.children.select { |n| n.is_a?(RXerces::XML::Element) }
170
+ first_person = people[0]
171
+ next_node = first_person.next_sibling
172
+
173
+ # May be a text node (whitespace) between elements
174
+ # Skip to next element if needed
175
+ while next_node && next_node.is_a?(RXerces::XML::Text)
176
+ next_node = next_node.next_sibling
177
+ end
178
+
179
+ expect(next_node).to be_a(RXerces::XML::Element)
180
+ expect(next_node.name).to eq('person')
181
+ expect(next_node['id']).to eq('2')
182
+ end
183
+
184
+ it "returns nil for last sibling" do
185
+ people = root.children.select { |n| n.is_a?(RXerces::XML::Element) }
186
+ last_person = people.last
187
+
188
+ # Navigate past any trailing whitespace
189
+ next_node = last_person.next_sibling
190
+ while next_node && next_node.is_a?(RXerces::XML::Text)
191
+ next_node = next_node.next_sibling
192
+ end
193
+
194
+ expect(next_node).to be_nil
195
+ end
196
+
197
+ it "can navigate through all siblings" do
198
+ first_element = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
199
+ siblings = []
200
+ current = first_element
201
+
202
+ while current
203
+ siblings << current if current.is_a?(RXerces::XML::Element)
204
+ current = current.next_sibling
205
+ end
206
+
207
+ expect(siblings.length).to eq(2)
208
+ expect(siblings[0]['id']).to eq('1')
209
+ expect(siblings[1]['id']).to eq('2')
210
+ end
211
+ end
212
+
213
+ describe "#previous_sibling" do
214
+ it "returns the previous sibling node" do
215
+ people = root.children.select { |n| n.is_a?(RXerces::XML::Element) }
216
+ second_person = people[1]
217
+ prev_node = second_person.previous_sibling
218
+
219
+ # May be a text node (whitespace) between elements
220
+ # Skip to previous element if needed
221
+ while prev_node && prev_node.is_a?(RXerces::XML::Text)
222
+ prev_node = prev_node.previous_sibling
223
+ end
224
+
225
+ expect(prev_node).to be_a(RXerces::XML::Element)
226
+ expect(prev_node.name).to eq('person')
227
+ expect(prev_node['id']).to eq('1')
228
+ end
229
+
230
+ it "returns nil for first sibling" do
231
+ first_element = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
232
+
233
+ # Navigate past any leading whitespace
234
+ prev_node = first_element.previous_sibling
235
+ while prev_node && prev_node.is_a?(RXerces::XML::Text)
236
+ prev_node = prev_node.previous_sibling
237
+ end
238
+
239
+ expect(prev_node).to be_nil
240
+ end
241
+
242
+ it "can navigate backward through all siblings" do
243
+ last_element = root.children.select { |n| n.is_a?(RXerces::XML::Element) }.last
244
+ siblings = []
245
+ current = last_element
246
+
247
+ while current
248
+ siblings.unshift(current) if current.is_a?(RXerces::XML::Element)
249
+ current = current.previous_sibling
250
+ end
251
+
252
+ expect(siblings.length).to eq(2)
253
+ expect(siblings[0]['id']).to eq('1')
254
+ expect(siblings[1]['id']).to eq('2')
255
+ end
256
+ end
257
+
258
+ describe "#add_child" do
259
+ it "adds a text node from a string" do
260
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
261
+ initial_count = person.children.length
262
+
263
+ person.add_child("New text content")
264
+
265
+ expect(person.children.length).to eq(initial_count + 1)
266
+ last_child = person.children.last
267
+ expect(last_child).to be_a(RXerces::XML::Text)
268
+ expect(last_child.text).to eq("New text content")
269
+ end
270
+
271
+ it "adds a new element to another element" do
272
+ # Create a simple document to test with
273
+ simple_xml = '<root><parent></parent></root>'
274
+ simple_doc = RXerces::XML::Document.parse(simple_xml)
275
+ parent = simple_doc.root.children.find { |n| n.is_a?(RXerces::XML::Element) }
276
+
277
+ # Add text child
278
+ parent.add_child("Hello World")
279
+
280
+ expect(parent.children.length).to be > 0
281
+ text_child = parent.children.find { |n| n.is_a?(RXerces::XML::Text) }
282
+ expect(text_child.text).to eq("Hello World")
283
+ end
284
+
285
+ it "allows building a document structure" do
286
+ simple_xml = '<root></root>'
287
+ simple_doc = RXerces::XML::Document.parse(simple_xml)
288
+ root = simple_doc.root
289
+
290
+ # Add multiple children
291
+ root.add_child("First text")
292
+ root.add_child("Second text")
293
+
294
+ text_nodes = root.children.select { |n| n.is_a?(RXerces::XML::Text) }
295
+ expect(text_nodes.length).to eq(2)
296
+ expect(text_nodes[0].text).to eq("First text")
297
+ expect(text_nodes[1].text).to eq("Second text")
298
+ end
299
+
300
+ it "modifies the document" do
301
+ simple_xml = '<item></item>'
302
+ simple_doc = RXerces::XML::Document.parse(simple_xml)
303
+ item = simple_doc.root
304
+
305
+ item.add_child("Content")
306
+
307
+ xml_output = simple_doc.to_s
308
+ expect(xml_output).to include("Content")
309
+ end
310
+ end
311
+
312
+ describe "#remove" do
313
+ it "removes a node from its parent" do
314
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
315
+ initial_count = root.children.select { |n| n.is_a?(RXerces::XML::Element) }.length
316
+
317
+ person.remove
318
+
319
+ remaining = root.children.select { |n| n.is_a?(RXerces::XML::Element) }
320
+ expect(remaining.length).to eq(initial_count - 1)
321
+ end
322
+
323
+ it "removes a child element from parent" do
324
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
325
+ age = person.children.find { |n| n.name == 'age' }
326
+ initial_count = person.children.select { |n| n.is_a?(RXerces::XML::Element) }.length
327
+
328
+ age.remove
329
+
330
+ remaining = person.children.select { |n| n.is_a?(RXerces::XML::Element) }
331
+ expect(remaining.length).to eq(initial_count - 1)
332
+ expect(person.children.find { |n| n.name == 'age' }).to be_nil
333
+ end
334
+
335
+ it "modifies the document" do
336
+ simple_xml = '<root><item>Remove me</item><keep>Keep me</keep></root>'
337
+ simple_doc = RXerces::XML::Document.parse(simple_xml)
338
+ item = simple_doc.xpath('//item').first
339
+
340
+ item.remove
341
+
342
+ xml_output = simple_doc.to_s
343
+ expect(xml_output).not_to include("Remove me")
344
+ expect(xml_output).to include("Keep me")
345
+ end
346
+
347
+ it "raises error when node has no parent" do
348
+ # The root element's parent is the document, so this should work
349
+ # We'll test with a document node instead
350
+ expect {
351
+ root.parent.remove
352
+ }.to raise_error(RuntimeError, /no parent/)
353
+ end
354
+ end
355
+
356
+ describe "#unlink" do
357
+ it "is an alias for remove" do
358
+ simple_xml = '<root><item>Test</item></root>'
359
+ simple_doc = RXerces::XML::Document.parse(simple_xml)
360
+ item = simple_doc.xpath('//item').first
361
+
362
+ expect(item).to respond_to(:unlink)
363
+ item.unlink
364
+
365
+ xml_output = simple_doc.to_s
366
+ expect(xml_output).not_to include("Test")
367
+ end
368
+ end
369
+
105
370
  describe "#xpath" do
106
371
  it "returns a NodeSet" do
107
372
  result = root.xpath('.//age')
108
373
  expect(result).to be_a(RXerces::XML::NodeSet)
109
374
  end
110
375
  end
376
+
377
+ describe "#inner_html" do
378
+ it "returns the XML content of children without parent tags" do
379
+ person = root.xpath('//person').first
380
+ inner = person.inner_html
381
+ expect(inner).to include('<age>')
382
+ expect(inner).to include('<city>')
383
+ expect(inner).not_to include('<person')
384
+ end
385
+
386
+ it "returns empty string for nodes without children" do
387
+ age = root.xpath('//age').first
388
+ inner = age.inner_html
389
+ expect(inner).to eq('30')
390
+ end
391
+
392
+ it "includes multiple children" do
393
+ person = root.xpath('//person').first
394
+ inner = person.inner_html
395
+ expect(inner).to include('<age>30</age>')
396
+ expect(inner).to include('<city>New York</city>')
397
+ end
398
+ end
399
+
400
+ describe "#inner_xml" do
401
+ it "is an alias for inner_html" do
402
+ person = root.xpath('//person').first
403
+ expect(person.inner_xml).to eq(person.inner_html)
404
+ end
405
+
406
+ it "returns the same XML content" do
407
+ person = root.xpath('//person').first
408
+ inner = person.inner_xml
409
+ expect(inner).to include('<age>')
410
+ expect(inner).to include('<city>')
411
+ end
412
+ end
413
+
414
+ describe "#path" do
415
+ it "returns the XPath to the root element" do
416
+ expect(root.path).to eq('/root[1]')
417
+ end
418
+
419
+ it "returns the XPath to a nested element" do
420
+ person = root.xpath('//person').first
421
+ expect(person.path).to eq('/root[1]/person[1]')
422
+ end
423
+
424
+ it "returns the XPath to the second person element" do
425
+ people = root.xpath('//person')
426
+ second_person = people[1]
427
+ expect(second_person.path).to eq('/root[1]/person[2]')
428
+ end
429
+
430
+ it "returns the XPath to deeply nested elements" do
431
+ age = root.xpath('//age').first
432
+ expect(age.path).to eq('/root[1]/person[1]/age[1]')
433
+ end
434
+ end
435
+
436
+ describe "#blank?" do
437
+ let(:blank_xml) { '<root><empty></empty><whitespace> </whitespace><content>Hello</content></root>' }
438
+ let(:blank_doc) { RXerces::XML::Document.parse(blank_xml) }
439
+
440
+ it "returns false for elements with text content" do
441
+ content = blank_doc.xpath('//content').first
442
+ expect(content.blank?).to be false
443
+ end
444
+
445
+ it "returns true for empty elements" do
446
+ empty = blank_doc.xpath('//empty').first
447
+ expect(empty.blank?).to be true
448
+ end
449
+
450
+ it "returns true for elements with only whitespace" do
451
+ whitespace = blank_doc.xpath('//whitespace').first
452
+ expect(whitespace.blank?).to be true
453
+ end
454
+
455
+ it "returns false for elements with child elements" do
456
+ root = blank_doc.root
457
+ expect(root.blank?).to be false
458
+ end
459
+
460
+ it "returns false for text nodes with content" do
461
+ content = blank_doc.xpath('//content').first
462
+ text_node = content.children.first
463
+ expect(text_node.blank?).to be false
464
+ end
465
+
466
+ it "returns true for text nodes with only whitespace" do
467
+ whitespace = blank_doc.xpath('//whitespace').first
468
+ text_node = whitespace.children.first
469
+ expect(text_node.blank?).to be true
470
+ end
471
+ end
111
472
  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
@@ -4,7 +4,7 @@ require 'rxerces'
4
4
 
5
5
  RSpec.shared_examples RXerces do
6
6
  example 'version number is set to the expected value' do
7
- expect(RXerces::VERSION).to eq('0.2.0')
7
+ expect(RXerces::VERSION).to eq('0.3.0')
8
8
  expect(RXerces::VERSION).to be_frozen
9
9
  end
10
10
  end
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RXerces::XML::Schema do
4
+ let(:simple_xsd) do
5
+ <<~XSD
6
+ <?xml version="1.0" encoding="UTF-8"?>
7
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
8
+ <xs:element name="root">
9
+ <xs:complexType>
10
+ <xs:sequence>
11
+ <xs:element name="name" type="xs:string"/>
12
+ <xs:element name="age" type="xs:integer"/>
13
+ </xs:sequence>
14
+ </xs:complexType>
15
+ </xs:element>
16
+ </xs:schema>
17
+ XSD
18
+ end
19
+
20
+ let(:valid_xml) do
21
+ <<~XML
22
+ <?xml version="1.0"?>
23
+ <root>
24
+ <name>John</name>
25
+ <age>30</age>
26
+ </root>
27
+ XML
28
+ end
29
+
30
+ let(:invalid_xml) do
31
+ <<~XML
32
+ <?xml version="1.0"?>
33
+ <root>
34
+ <name>John</name>
35
+ <age>not-a-number</age>
36
+ </root>
37
+ XML
38
+ end
39
+
40
+ describe '.from_string' do
41
+ it 'creates a schema from an XSD string' do
42
+ schema = described_class.from_string(simple_xsd)
43
+ expect(schema).to be_a(described_class)
44
+ end
45
+
46
+ # Note: Xerces-C parser is very tolerant of invalid XML
47
+ # So we just skip testing for invalid XML for now
48
+ end
49
+
50
+ describe '.from_document' do
51
+ it 'creates a schema from a Document' do
52
+ schema_doc = RXerces::XML::Document.parse(simple_xsd)
53
+ schema = described_class.from_document(schema_doc)
54
+ expect(schema).to be_a(described_class)
55
+ end
56
+ end
57
+
58
+ describe 'validation' do
59
+ let(:schema) { described_class.from_string(simple_xsd) }
60
+
61
+ it 'validates a valid document' do
62
+ doc = RXerces::XML::Document.parse(valid_xml)
63
+ errors = doc.validate(schema)
64
+ expect(errors).to be_a(Array)
65
+ expect(errors).to be_empty
66
+ end
67
+
68
+ it 'returns validation errors for an invalid document' do
69
+ doc = RXerces::XML::Document.parse(invalid_xml)
70
+ errors = doc.validate(schema)
71
+ expect(errors).to be_a(Array)
72
+ expect(errors).not_to be_empty
73
+ expect(errors.first).to include('not-a-number')
74
+ end
75
+ end
76
+ end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rxerces
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel J. Berger
@@ -93,6 +93,7 @@ files:
93
93
  - Rakefile
94
94
  - certs/djberg96_pub.pem
95
95
  - examples/basic_usage.rb
96
+ - examples/schema_example.rb
96
97
  - examples/simple_example.rb
97
98
  - examples/xpath_example.rb
98
99
  - ext/rxerces/extconf.rb
@@ -109,6 +110,7 @@ files:
109
110
  - spec/nokogiri_compatibility_spec.rb
110
111
  - spec/rxerces_shared.rb
111
112
  - spec/rxerces_spec.rb
113
+ - spec/schema_spec.rb
112
114
  - spec/spec_helper.rb
113
115
  - spec/xpath_spec.rb
114
116
  homepage: http://github.com/djberg96/rxerces
@@ -148,4 +150,5 @@ test_files:
148
150
  - spec/nodeset_spec.rb
149
151
  - spec/nokogiri_compatibility_spec.rb
150
152
  - spec/rxerces_spec.rb
153
+ - spec/schema_spec.rb
151
154
  - spec/xpath_spec.rb
metadata.gz.sig CHANGED
@@ -1,2 +1,2 @@
1
- *P�]���/���d�E���/'�*L��Ư��?]ZmZu,�_W����q���jí�Ͷ2O �1���{��iI��7���"� �����+��b�>�B���
2
- s��P}(]�����J�4����kӺP^��&9m����[N3i�pֲ�MK���Ǒ��D� �Qz��%eR���.|U�u�6O!ku�ma���Á}��W~���/z�;�n��HU�
1
+ Q� �q��o �{�;?��-6��%�:��-ܪB�h�y,�P�U}�#loyp�w��c����M@X+>��]U�.��<z5��a'�mz]G���1laAx���wV�`���b6��U�����s��z��=u��$�6��Vj6��~��x:�����
2
+ O��Е>d~�s��cYs���� ������tHgd�%Y��@TtcԁI��S