rng 0.1.2 → 0.3.3
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
- data/.github/workflows/docs.yml +63 -0
- data/.github/workflows/release.yml +8 -3
- data/.gitignore +11 -0
- data/.rubocop.yml +10 -7
- data/.rubocop_todo.yml +229 -23
- data/CHANGELOG.md +317 -0
- data/CLAUDE.md +139 -0
- data/Gemfile +11 -12
- data/README.adoc +1538 -11
- data/Rakefile +11 -3
- data/docs/Gemfile +8 -0
- data/docs/_config.yml +23 -0
- data/docs/getting-started/index.adoc +75 -0
- data/docs/guides/error-handling.adoc +137 -0
- data/docs/guides/external-references.adoc +128 -0
- data/docs/guides/index.adoc +24 -0
- data/docs/guides/parsing-rnc.adoc +141 -0
- data/docs/guides/parsing-rng-xml.adoc +81 -0
- data/docs/guides/rng-to-rnc.adoc +101 -0
- data/docs/guides/validation.adoc +85 -0
- data/docs/index.adoc +52 -0
- data/docs/reference/api.adoc +126 -0
- data/docs/reference/cli.adoc +182 -0
- data/docs/understanding/architecture.adoc +58 -0
- data/docs/understanding/rng-vs-rnc.adoc +118 -0
- data/exe/rng +5 -0
- data/lib/rng/any_name.rb +10 -8
- data/lib/rng/attribute.rb +28 -26
- data/lib/rng/choice.rb +24 -24
- data/lib/rng/cli.rb +607 -0
- data/lib/rng/data.rb +10 -10
- data/lib/rng/datatype_declaration.rb +26 -0
- data/lib/rng/define.rb +44 -41
- data/lib/rng/div.rb +36 -0
- data/lib/rng/documentation.rb +9 -0
- data/lib/rng/element.rb +39 -37
- data/lib/rng/empty.rb +7 -7
- data/lib/rng/except.rb +25 -25
- data/lib/rng/external_ref.rb +8 -8
- data/lib/rng/external_ref_resolver.rb +582 -0
- data/lib/rng/foreign_attribute.rb +26 -0
- data/lib/rng/foreign_element.rb +33 -0
- data/lib/rng/grammar.rb +14 -12
- data/lib/rng/group.rb +26 -24
- data/lib/rng/include.rb +5 -6
- data/lib/rng/include_processor.rb +461 -0
- data/lib/rng/interleave.rb +23 -23
- data/lib/rng/list.rb +22 -22
- data/lib/rng/mixed.rb +23 -23
- data/lib/rng/name.rb +7 -7
- data/lib/rng/namespace_declaration.rb +47 -0
- data/lib/rng/namespaces.rb +15 -0
- data/lib/rng/not_allowed.rb +7 -7
- data/lib/rng/ns_name.rb +9 -9
- data/lib/rng/one_or_more.rb +23 -23
- data/lib/rng/optional.rb +23 -23
- data/lib/rng/param.rb +8 -8
- data/lib/rng/parent_ref.rb +8 -8
- data/lib/rng/parse_tree_processor.rb +695 -0
- data/lib/rng/pattern.rb +7 -7
- data/lib/rng/ref.rb +8 -8
- data/lib/rng/rnc_builder.rb +927 -0
- data/lib/rng/rnc_parser.rb +605 -305
- data/lib/rng/rnc_to_rng_converter.rb +1408 -0
- data/lib/rng/schema_preamble.rb +73 -0
- data/lib/rng/schema_validator.rb +1622 -0
- data/lib/rng/start.rb +27 -25
- data/lib/rng/test_suite_parser.rb +168 -0
- data/lib/rng/text.rb +11 -8
- data/lib/rng/to_rnc.rb +4 -35
- data/lib/rng/value.rb +6 -7
- data/lib/rng/version.rb +1 -1
- data/lib/rng/zero_or_more.rb +23 -23
- data/lib/rng.rb +68 -17
- data/rng.gemspec +18 -19
- data/scripts/extract_spectest_resources.rb +96 -0
- data/spec/fixtures/compacttest.xml +2511 -0
- data/spec/fixtures/external/circular_a.rng +7 -0
- data/spec/fixtures/external/circular_b.rng +7 -0
- data/spec/fixtures/external/circular_main.rng +7 -0
- data/spec/fixtures/external/external_ref_lib.rng +7 -0
- data/spec/fixtures/external/external_ref_main.rng +7 -0
- data/spec/fixtures/external/include_lib.rng +7 -0
- data/spec/fixtures/external/include_main.rng +3 -0
- data/spec/fixtures/external/nested_chain.rng +6 -0
- data/spec/fixtures/external/nested_leaf.rng +7 -0
- data/spec/fixtures/external/nested_mid.rng +8 -0
- data/spec/fixtures/metanorma/3gpp.rnc +35 -0
- data/spec/fixtures/metanorma/3gpp.rng +105 -0
- data/spec/fixtures/metanorma/basicdoc.rnc +11 -0
- data/spec/fixtures/metanorma/bipm.rnc +148 -0
- data/spec/fixtures/metanorma/bipm.rng +376 -0
- data/spec/fixtures/metanorma/bsi.rnc +104 -0
- data/spec/fixtures/metanorma/bsi.rng +332 -0
- data/spec/fixtures/metanorma/csa.rnc +45 -0
- data/spec/fixtures/metanorma/csa.rng +131 -0
- data/spec/fixtures/metanorma/csd.rnc +43 -0
- data/spec/fixtures/metanorma/csd.rng +132 -0
- data/spec/fixtures/metanorma/gbstandard.rnc +99 -0
- data/spec/fixtures/metanorma/gbstandard.rng +316 -0
- data/spec/fixtures/metanorma/iec.rnc +49 -0
- data/spec/fixtures/metanorma/iec.rng +193 -0
- data/spec/fixtures/metanorma/ietf.rnc +275 -0
- data/spec/fixtures/metanorma/ietf.rng +925 -0
- data/spec/fixtures/metanorma/iho.rnc +58 -0
- data/spec/fixtures/metanorma/iho.rng +179 -0
- data/spec/fixtures/metanorma/isodoc.rnc +873 -0
- data/spec/fixtures/metanorma/isodoc.rng +2704 -0
- data/spec/fixtures/metanorma/isostandard-amd.rnc +43 -0
- data/spec/fixtures/metanorma/isostandard-amd.rng +108 -0
- data/spec/fixtures/metanorma/isostandard.rnc +166 -0
- data/spec/fixtures/metanorma/isostandard.rng +494 -0
- data/spec/fixtures/metanorma/itu.rnc +122 -0
- data/spec/fixtures/metanorma/itu.rng +377 -0
- data/spec/fixtures/metanorma/m3d.rnc +41 -0
- data/spec/fixtures/metanorma/m3d.rng +122 -0
- data/spec/fixtures/metanorma/mpfd.rnc +36 -0
- data/spec/fixtures/metanorma/mpfd.rng +95 -0
- data/spec/fixtures/metanorma/nist.rnc +77 -0
- data/spec/fixtures/metanorma/nist.rng +216 -0
- data/spec/fixtures/metanorma/ogc.rnc +51 -0
- data/spec/fixtures/metanorma/ogc.rng +151 -0
- data/spec/fixtures/metanorma/reqt.rnc +6 -0
- data/spec/fixtures/metanorma/rsd.rnc +36 -0
- data/spec/fixtures/metanorma/rsd.rng +95 -0
- data/spec/fixtures/metanorma/un.rnc +103 -0
- data/spec/fixtures/metanorma/un.rng +367 -0
- data/spec/fixtures/rnc/base.rnc +4 -0
- data/spec/fixtures/rnc/grammar_with_trailing.rnc +8 -0
- data/spec/fixtures/rnc/main_include_trailing.rnc +3 -0
- data/spec/fixtures/rnc/main_with_include.rnc +5 -0
- data/spec/fixtures/rnc/test_augment.rnc +10 -0
- data/spec/fixtures/rnc/test_isodoc_simple.rnc +9 -0
- data/spec/fixtures/rnc/top_level_include.rnc +8 -0
- data/spec/fixtures/spectest_external/case_10_4.7/x +3 -0
- data/spec/fixtures/spectest_external/case_10_4.7/y +7 -0
- data/spec/fixtures/spectest_external/case_11_4.7/x +3 -0
- data/spec/fixtures/spectest_external/case_12_4.7/x +3 -0
- data/spec/fixtures/spectest_external/case_13_4.7/x +3 -0
- data/spec/fixtures/spectest_external/case_13_4.7/y +3 -0
- data/spec/fixtures/spectest_external/case_14_4.7/x +7 -0
- data/spec/fixtures/spectest_external/case_15_4.7/x +7 -0
- data/spec/fixtures/spectest_external/case_16_4.7/x +5 -0
- data/spec/fixtures/spectest_external/case_17_4.7/x +5 -0
- data/spec/fixtures/spectest_external/case_18_4.7/x +7 -0
- data/spec/fixtures/spectest_external/case_19_4.7/level1.rng +9 -0
- data/spec/fixtures/spectest_external/case_19_4.7/level2.rng +7 -0
- data/spec/fixtures/spectest_external/case_1_4.5/sub1/x +3 -0
- data/spec/fixtures/spectest_external/case_1_4.5/sub3/x +3 -0
- data/spec/fixtures/spectest_external/case_1_4.5/x +3 -0
- data/spec/fixtures/spectest_external/case_20_4.6/x +3 -0
- data/spec/fixtures/spectest_external/case_2_4.5/x +3 -0
- data/spec/fixtures/spectest_external/case_3_4.6/x +3 -0
- data/spec/fixtures/spectest_external/case_4_4.6/x +3 -0
- data/spec/fixtures/spectest_external/case_5_4.6/x +1 -0
- data/spec/fixtures/spectest_external/case_6_4.6/x +5 -0
- data/spec/fixtures/spectest_external/case_7_4.6/x +1 -0
- data/spec/fixtures/spectest_external/case_7_4.6/y +1 -0
- data/spec/fixtures/spectest_external/case_8_4.7/x +7 -0
- data/spec/fixtures/spectest_external/case_9_4.7/x +7 -0
- data/spec/fixtures/spectest_external/resources.json +149 -0
- data/spec/rng/advanced_rnc_spec.rb +101 -0
- data/spec/rng/compacttest_spec.rb +197 -0
- data/spec/rng/datatype_declaration_spec.rb +28 -0
- data/spec/rng/div_spec.rb +207 -0
- data/spec/rng/external_ref_resolver_spec.rb +122 -0
- data/spec/rng/metanorma_conversion_spec.rb +159 -0
- data/spec/rng/namespace_declaration_spec.rb +60 -0
- data/spec/rng/namespace_support_spec.rb +199 -0
- data/spec/rng/rnc_parser_spec.rb +498 -22
- data/spec/rng/rnc_roundtrip_spec.rb +96 -82
- data/spec/rng/rng_generation_spec.rb +288 -0
- data/spec/rng/roundtrip_spec.rb +342 -0
- data/spec/rng/schema_preamble_spec.rb +145 -0
- data/spec/rng/schema_spec.rb +68 -64
- data/spec/rng/spectest_spec.rb +168 -90
- data/spec/rng_spec.rb +2 -2
- data/spec/spec_helper.rb +7 -42
- metadata +141 -8
data/spec/rng/schema_spec.rb
CHANGED
|
@@ -1,142 +1,146 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
RSpec.describe Rng::Grammar do
|
|
6
|
-
describe
|
|
6
|
+
describe 'RNG parsing' do
|
|
7
7
|
let(:rng_input) do
|
|
8
|
-
File.read(
|
|
8
|
+
File.read('spec/fixtures/rng/address_book.rng')
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
it
|
|
11
|
+
it 'correctly parses RNG' do
|
|
12
12
|
parsed = Rng.parse(rng_input)
|
|
13
|
-
expect(parsed).to be_a(
|
|
13
|
+
expect(parsed).to be_a(described_class)
|
|
14
14
|
expect(parsed.element).to be_empty
|
|
15
|
-
expect(parsed.start.element.attr_name).to eq(
|
|
15
|
+
expect(parsed.start.first.element.attr_name).to eq('addressBook')
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
describe
|
|
19
|
+
describe 'Round-trip testing RNG' do
|
|
20
20
|
# Address Book Tests
|
|
21
21
|
let(:address_book_rng) do
|
|
22
|
-
File.read(
|
|
22
|
+
File.read('spec/fixtures/rng/address_book.rng')
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
expect(regenerated).to be_analogous_with(address_book_rng)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
it "correctly round-trips address_book.rng (formatted equivalent comparison)" do
|
|
32
|
-
parsed = Rng::Grammar.from_xml(address_book_rng)
|
|
33
|
-
regenerated = parsed.to_xml
|
|
34
|
-
expect(regenerated).to be_equivalent_to_xml(address_book_rng)
|
|
25
|
+
# Test Suite Schema Tests
|
|
26
|
+
let(:test_suite_rng) do
|
|
27
|
+
File.read('spec/fixtures/rng/testSuite.rng')
|
|
35
28
|
end
|
|
36
|
-
|
|
37
29
|
# RELAX NG Schema Tests
|
|
38
30
|
let(:relaxng_schema) do
|
|
39
|
-
File.read(
|
|
31
|
+
File.read('spec/fixtures/rng/relaxng.rng')
|
|
40
32
|
end
|
|
41
33
|
|
|
42
|
-
it
|
|
43
|
-
parsed =
|
|
34
|
+
it 'correctly round-trips address_book.rng (analogous comparison)' do
|
|
35
|
+
parsed = described_class.from_xml(address_book_rng)
|
|
44
36
|
regenerated = parsed.to_xml
|
|
45
|
-
expect(regenerated).to
|
|
37
|
+
expect(regenerated.gsub(/<!--.*?-->/m, '')).to be_xml_equivalent_to(address_book_rng.gsub(/<!--.*?-->/m, ''))
|
|
46
38
|
end
|
|
47
39
|
|
|
48
|
-
it
|
|
49
|
-
parsed =
|
|
40
|
+
it 'correctly round-trips address_book.rng (formatted equivalent comparison)' do
|
|
41
|
+
parsed = described_class.from_xml(address_book_rng)
|
|
50
42
|
regenerated = parsed.to_xml
|
|
51
|
-
expect(regenerated).to
|
|
43
|
+
expect(regenerated).to be_xml_equivalent_to(address_book_rng)
|
|
52
44
|
end
|
|
53
45
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
it 'correctly round-trips relaxng.rng (analogous comparison)' do
|
|
47
|
+
parsed = described_class.from_xml(relaxng_schema)
|
|
48
|
+
regenerated = parsed.to_xml
|
|
49
|
+
# Strip comments for comparison since Lutaml doesn't preserve them
|
|
50
|
+
expect(regenerated.gsub(/<!--.*?-->/m, '')).to be_xml_equivalent_to(relaxng_schema.gsub(/<!--.*?-->/m, ''))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'correctly round-trips relaxng.rng (formatted equivalent comparison)' do
|
|
54
|
+
parsed = described_class.from_xml(relaxng_schema)
|
|
55
|
+
regenerated = parsed.to_xml
|
|
56
|
+
expect(regenerated.gsub(/<!--.*?-->/m, '')).to be_xml_equivalent_to(relaxng_schema.gsub(/<!--.*?-->/m, ''))
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
it
|
|
60
|
-
parsed =
|
|
59
|
+
it 'correctly round-trips testSuite.rng (analogous comparison)' do
|
|
60
|
+
parsed = described_class.from_xml(test_suite_rng)
|
|
61
61
|
regenerated = parsed.to_xml
|
|
62
|
-
expect(regenerated).to
|
|
62
|
+
expect(regenerated.gsub(/<!--.*?-->/m, '')).to be_xml_equivalent_to(test_suite_rng.gsub(/<!--.*?-->/m, ''))
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
it
|
|
66
|
-
parsed =
|
|
65
|
+
it 'correctly round-trips testSuite.rng (formatted equivalent comparison)' do
|
|
66
|
+
parsed = described_class.from_xml(test_suite_rng)
|
|
67
67
|
regenerated = parsed.to_xml
|
|
68
|
-
expect(regenerated).to
|
|
68
|
+
expect(regenerated.gsub(/<!--.*?-->/m, '')).to be_xml_equivalent_to(test_suite_rng.gsub(/<!--.*?-->/m, ''))
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
describe 'RNC parsing' do
|
|
73
73
|
let(:rnc_input) do
|
|
74
|
-
File.read(
|
|
74
|
+
File.read('spec/fixtures/rnc/address_book.rnc')
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
it
|
|
77
|
+
it 'correctly parses RNC' do
|
|
78
78
|
parsed = Rng.parse_rnc(rnc_input)
|
|
79
|
-
expect(parsed).to be_a(
|
|
80
|
-
|
|
81
|
-
expect(
|
|
79
|
+
expect(parsed).to be_a(described_class)
|
|
80
|
+
start_element = parsed.start.first.element
|
|
81
|
+
expect(start_element).to be_a(Rng::Element)
|
|
82
|
+
expect(start_element.attr_name).to eq('addressBook')
|
|
82
83
|
end
|
|
83
84
|
end
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
describe 'RNG to RNC conversion' do
|
|
86
87
|
let(:rng_input) do
|
|
87
|
-
File.read(
|
|
88
|
+
File.read('spec/fixtures/rng/address_book.rng')
|
|
88
89
|
end
|
|
89
90
|
|
|
90
|
-
it
|
|
91
|
+
it 'correctly converts RNG to RNC' do
|
|
91
92
|
parsed = Rng.parse(rng_input)
|
|
92
93
|
rnc = Rng.to_rnc(parsed)
|
|
93
|
-
expect(rnc).to include(
|
|
94
|
-
expect(rnc).to include(
|
|
95
|
-
expect(rnc).to include(
|
|
96
|
-
expect(rnc).to include(
|
|
97
|
-
expect(rnc).to include("element note { text }?")
|
|
94
|
+
expect(rnc).to include('element addressBook')
|
|
95
|
+
expect(rnc).to include('element card')
|
|
96
|
+
expect(rnc).to include('element name')
|
|
97
|
+
expect(rnc).to include('element email')
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
describe 'RNC to RNG conversion' do
|
|
102
102
|
let(:rnc_input) do
|
|
103
|
-
File.read(
|
|
103
|
+
File.read('spec/fixtures/rnc/address_book.rnc')
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
it
|
|
106
|
+
it 'correctly converts RNC to RNG' do
|
|
107
107
|
parsed = Rng.parse_rnc(rnc_input)
|
|
108
|
-
expect(parsed).to be_a(
|
|
109
|
-
|
|
110
|
-
expect(
|
|
108
|
+
expect(parsed).to be_a(described_class)
|
|
109
|
+
start_element = parsed.start.first.element
|
|
110
|
+
expect(start_element).to be_a(Rng::Element)
|
|
111
|
+
expect(start_element.attr_name).to eq('addressBook')
|
|
111
112
|
end
|
|
112
113
|
end
|
|
113
114
|
|
|
114
|
-
|
|
115
|
+
describe 'Round-trip testing RNG/RNC' do
|
|
115
116
|
let(:rng_input) do
|
|
116
|
-
File.read(
|
|
117
|
+
File.read('spec/fixtures/rng/address_book.rng')
|
|
117
118
|
end
|
|
118
119
|
|
|
119
120
|
let(:rnc_input) do
|
|
120
|
-
File.read(
|
|
121
|
+
File.read('spec/fixtures/rnc/address_book.rnc')
|
|
121
122
|
end
|
|
122
123
|
|
|
123
|
-
it
|
|
124
|
+
it 'correctly round-trips RNG to RNC and back' do
|
|
124
125
|
parsed_rng = Rng.parse(rng_input)
|
|
125
126
|
rnc = Rng.to_rnc(parsed_rng)
|
|
126
127
|
parsed_rnc = Rng.parse_rnc(rnc)
|
|
127
128
|
|
|
128
129
|
# Compare key properties
|
|
129
|
-
|
|
130
|
+
rng_start_elem = parsed_rng.start.first.element
|
|
131
|
+
rnc_start_elem = parsed_rnc.start.first.element
|
|
132
|
+
expect(rnc_start_elem.attr_name).to eq(rng_start_elem.attr_name)
|
|
130
133
|
end
|
|
131
134
|
|
|
132
|
-
it
|
|
135
|
+
it 'correctly round-trips RNC to RNG and back' do
|
|
133
136
|
parsed_rnc = Rng.parse_rnc(rnc_input)
|
|
134
|
-
rng_xml =
|
|
137
|
+
rng_xml = parsed_rnc.to_xml
|
|
135
138
|
parsed_rng = Rng.parse(rng_xml)
|
|
136
|
-
Rng.to_rnc(parsed_rng)
|
|
137
139
|
|
|
138
140
|
# Compare key properties
|
|
139
|
-
|
|
141
|
+
rng_start_elem = parsed_rng.start.first.element
|
|
142
|
+
rnc_start_elem = parsed_rnc.start.first.element
|
|
143
|
+
expect(rng_start_elem.attr_name).to eq(rnc_start_elem.attr_name)
|
|
140
144
|
end
|
|
141
145
|
end
|
|
142
146
|
end
|
data/spec/rng/spectest_spec.rb
CHANGED
|
@@ -1,23 +1,113 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'nokogiri'
|
|
5
|
+
require 'tmpdir'
|
|
5
6
|
|
|
6
7
|
# Load the test suite XML file once for all tests
|
|
7
|
-
SPEC_TEST_XML_PATH =
|
|
8
|
-
|
|
8
|
+
SPEC_TEST_XML_PATH = 'spec/fixtures/spectest.xml'
|
|
9
|
+
SPECTEST_XML = Nokogiri::XML(File.read(SPEC_TEST_XML_PATH))
|
|
10
|
+
|
|
11
|
+
# Load resources mapping if available
|
|
12
|
+
SPECTEST_RESOURCES_PATH = 'spec/fixtures/spectest_external/resources.json'
|
|
13
|
+
SPECTEST_RESOURCES = File.exist?(SPECTEST_RESOURCES_PATH) ? JSON.parse(File.read(SPECTEST_RESOURCES_PATH)) : {}
|
|
14
|
+
|
|
15
|
+
# Helper to extract resources from a test case element
|
|
16
|
+
def extract_resources_from_test_case(test_case)
|
|
17
|
+
resources = {}
|
|
18
|
+
|
|
19
|
+
# Handle resources in directories
|
|
20
|
+
test_case.xpath('.//dir').each do |dir|
|
|
21
|
+
dir_name = dir['name']
|
|
22
|
+
dir.xpath('.//resource').each do |res|
|
|
23
|
+
resource_name = res['name']
|
|
24
|
+
content = res.inner_html.strip
|
|
25
|
+
resources["#{dir_name}/#{resource_name}"] = content unless content.empty?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
9
28
|
|
|
10
|
-
|
|
11
|
-
|
|
29
|
+
# Handle resources at root level
|
|
30
|
+
test_case.xpath('./resource').each do |res|
|
|
31
|
+
resource_name = res['name']
|
|
32
|
+
content = res.inner_html.strip
|
|
33
|
+
resources[resource_name] = content unless content.empty?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
resources
|
|
37
|
+
end
|
|
12
38
|
|
|
13
|
-
|
|
39
|
+
# Helper to check if schema has external refs
|
|
40
|
+
def schema_has_external_refs?(schema_xml)
|
|
41
|
+
(schema_xml.include?('<externalRef') && schema_xml.include?('href="')) ||
|
|
42
|
+
(schema_xml.include?('<include') && schema_xml.include?('href="'))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Helper to run a schema test with external ref resolution
|
|
46
|
+
def run_schema_test(schema_xml, resources, expect_error: false)
|
|
47
|
+
if resources.empty? && schema_has_external_refs?(schema_xml)
|
|
48
|
+
return { skipped: true,
|
|
49
|
+
reason: 'no resources for external refs' }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if schema_has_external_refs?(schema_xml) && !resources.empty?
|
|
53
|
+
# Set up temp directory with resources
|
|
54
|
+
Dir.mktmpdir do |tmpdir|
|
|
55
|
+
# Write resources to temp dir
|
|
56
|
+
resources.each do |name, content|
|
|
57
|
+
file_path = File.join(tmpdir, name)
|
|
58
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
|
59
|
+
File.write(file_path, content)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Create a dummy schema file for location
|
|
63
|
+
schema_path = File.join(tmpdir, 'schema.rng')
|
|
64
|
+
File.write(schema_path, schema_xml)
|
|
65
|
+
|
|
66
|
+
# Parse with external ref resolution
|
|
67
|
+
begin
|
|
68
|
+
Rng.parse(schema_xml, location: schema_path, resolve_external: true)
|
|
69
|
+
|
|
70
|
+
if expect_error
|
|
71
|
+
{ passed: false, error: 'Expected validation error but schema parsed successfully' }
|
|
72
|
+
else
|
|
73
|
+
{ passed: true }
|
|
74
|
+
end
|
|
75
|
+
rescue Rng::SchemaValidationError => e
|
|
76
|
+
if expect_error
|
|
77
|
+
{ passed: true }
|
|
78
|
+
else
|
|
79
|
+
{ passed: false, error: "Unexpected validation error: #{e.message}" }
|
|
80
|
+
end
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
{ passed: false, error: "#{e.class}: #{e.message}" }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
else
|
|
86
|
+
# No external refs, just validate as-is
|
|
87
|
+
begin
|
|
88
|
+
Rng::SchemaValidator.validate(schema_xml)
|
|
89
|
+
if expect_error
|
|
90
|
+
{ passed: false, error: 'Expected validation error but schema validated' }
|
|
91
|
+
else
|
|
92
|
+
{ passed: true }
|
|
93
|
+
end
|
|
94
|
+
rescue Rng::SchemaValidationError
|
|
95
|
+
if expect_error
|
|
96
|
+
{ passed: true }
|
|
97
|
+
else
|
|
98
|
+
{ passed: false, error: 'Unexpected validation error' }
|
|
99
|
+
end
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
{ passed: false, error: "#{e.class}: #{e.message}" }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
14
104
|
end
|
|
15
105
|
|
|
16
106
|
# This helper function processes a test suite recursively and generates tests
|
|
17
|
-
def process_test_suite(suite_element, context_description =
|
|
107
|
+
def process_test_suite(suite_element, context_description = '', test_case_counter: 0)
|
|
18
108
|
# Get documentation or section number for the context description
|
|
19
|
-
documentation = suite_element.xpath(
|
|
20
|
-
section = suite_element.xpath(
|
|
109
|
+
documentation = suite_element.xpath('./documentation').text.strip
|
|
110
|
+
section = suite_element.xpath('./section').text.strip
|
|
21
111
|
|
|
22
112
|
# Build context description
|
|
23
113
|
context_desc = context_description
|
|
@@ -28,14 +118,15 @@ def process_test_suite(suite_element, context_description = "")
|
|
|
28
118
|
end
|
|
29
119
|
|
|
30
120
|
# Use a non-empty description
|
|
31
|
-
context_desc =
|
|
121
|
+
context_desc = 'RELAX NG Test Suite' if context_desc.empty?
|
|
32
122
|
|
|
33
123
|
# Generate tests within a context
|
|
34
124
|
context(context_desc) do
|
|
35
125
|
# Process all test cases in this suite
|
|
36
|
-
suite_element.xpath(
|
|
37
|
-
|
|
38
|
-
|
|
126
|
+
suite_element.xpath('./testCase').each_with_index do |test_case, index|
|
|
127
|
+
test_case_counter += 1
|
|
128
|
+
test_documentation = test_case.xpath('./documentation').text.strip
|
|
129
|
+
test_section = test_case.xpath('./section')&.text || ''
|
|
39
130
|
|
|
40
131
|
# Create descriptive test name
|
|
41
132
|
test_desc = if !test_documentation.empty?
|
|
@@ -43,56 +134,51 @@ def process_test_suite(suite_element, context_description = "")
|
|
|
43
134
|
elsif !test_section.empty?
|
|
44
135
|
"Section #{test_section} compliance"
|
|
45
136
|
else
|
|
46
|
-
|
|
137
|
+
'Schema validation'
|
|
47
138
|
end
|
|
48
139
|
|
|
49
|
-
#
|
|
50
|
-
|
|
140
|
+
# Extract resources for this test case
|
|
141
|
+
resources = extract_resources_from_test_case(test_case)
|
|
51
142
|
|
|
52
143
|
context(test_desc) do
|
|
53
144
|
# Test correct schemas
|
|
54
|
-
test_case.xpath(
|
|
145
|
+
test_case.xpath('./correct').each do |correct_schema|
|
|
55
146
|
schema_xml = correct_schema.inner_html.strip
|
|
56
147
|
|
|
57
148
|
# Skip empty schemas
|
|
58
149
|
next if schema_xml.empty?
|
|
59
150
|
|
|
60
|
-
# Store for validation tests
|
|
61
|
-
last_correct_schema_xml = schema_xml
|
|
62
|
-
|
|
63
151
|
it "#{test_desc} - correct schema parsing ##{index + 1}" do
|
|
64
|
-
skip_if_foreign(test_desc)
|
|
65
|
-
|
|
66
152
|
schema = Rng::Grammar.from_xml(schema_xml)
|
|
67
153
|
expect(schema).not_to be_nil
|
|
68
154
|
rescue StandardError => e
|
|
69
155
|
raise "Expected schema to be valid but got: #{e.message}\nSchema:\n#{schema_xml}"
|
|
70
156
|
end
|
|
71
157
|
|
|
72
|
-
# Add round-trip test
|
|
73
158
|
it "#{test_desc} - correct schema round-trip ##{index + 1}" do
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
159
|
+
# Check for foreign elements/attributes (lutaml-model drops these)
|
|
160
|
+
doc = Nokogiri::XML(schema_xml)
|
|
161
|
+
has_foreign = doc.xpath("//*[namespace-uri() != 'http://relaxng.org/ns/structure/1.0']").any? ||
|
|
162
|
+
doc.xpath("//@*[namespace-uri() != '' and namespace-uri() != 'http://relaxng.org/ns/structure/1.0' and local-name() != 'base']").any?
|
|
163
|
+
skip 'foreign elements/attributes not supported (by design)' if has_foreign
|
|
77
164
|
|
|
78
165
|
# Parse the XML into a schema
|
|
79
|
-
# Parse the XML to determine the root element name
|
|
80
166
|
xml_doc = Nokogiri::XML(schema_xml)
|
|
81
167
|
root_element_name = xml_doc.root&.name
|
|
82
168
|
|
|
83
169
|
# Choose the appropriate class based on the root element name
|
|
84
170
|
schema = case root_element_name
|
|
85
|
-
when
|
|
171
|
+
when 'grammar'
|
|
86
172
|
Rng::Grammar.from_xml(schema_xml)
|
|
87
|
-
when
|
|
173
|
+
when 'element'
|
|
88
174
|
Rng::Element.from_xml(schema_xml)
|
|
89
|
-
when
|
|
175
|
+
when 'group'
|
|
90
176
|
Rng::Group.from_xml(schema_xml)
|
|
91
|
-
when
|
|
177
|
+
when 'choice'
|
|
92
178
|
Rng::Choice.from_xml(schema_xml)
|
|
93
|
-
when
|
|
179
|
+
when 'notAllowed'
|
|
94
180
|
Rng::NotAllowed.from_xml(schema_xml)
|
|
95
|
-
when
|
|
181
|
+
when 'externalRef'
|
|
96
182
|
Rng::ExternalRef.from_xml(schema_xml)
|
|
97
183
|
else
|
|
98
184
|
# Default to Schema for other cases
|
|
@@ -103,87 +189,79 @@ def process_test_suite(suite_element, context_description = "")
|
|
|
103
189
|
regenerated = schema.to_xml
|
|
104
190
|
|
|
105
191
|
# Verify the regenerated XML matches the original
|
|
106
|
-
expect(regenerated).to
|
|
192
|
+
expect(regenerated).to be_xml_equivalent_to(schema_xml)
|
|
107
193
|
end
|
|
108
194
|
end
|
|
109
195
|
|
|
110
196
|
# Test incorrect schemas
|
|
111
|
-
test_case.xpath(
|
|
197
|
+
test_case.xpath('./incorrect').each do |incorrect_schema|
|
|
112
198
|
schema_xml = incorrect_schema.inner_html.strip
|
|
113
199
|
|
|
114
200
|
# Skip empty schemas
|
|
115
201
|
next if schema_xml.empty?
|
|
116
202
|
|
|
117
203
|
it "#{test_desc} - incorrect schema ##{index + 1}" do
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
204
|
+
has_external_refs = schema_has_external_refs?(schema_xml)
|
|
205
|
+
|
|
206
|
+
# NCName tests with Thai char U+0E35 are valid in XML 1.0 5th ed
|
|
207
|
+
# but invalid in RELAX NG spec (October 26 version used stricter rules)
|
|
208
|
+
skip 'NCName with Thai char U+0E35: XML 1.0 5th ed allows but older RELAX NG spec did not' if schema_xml.include?('ี') || schema_xml.include?("\xE35")
|
|
209
|
+
|
|
210
|
+
skip 'externalRef/include href resolution requires external file I/O' if has_external_refs && resources.empty?
|
|
211
|
+
|
|
212
|
+
result = run_schema_test(schema_xml, resources, expect_error: true)
|
|
213
|
+
|
|
214
|
+
if result[:skipped]
|
|
215
|
+
skip result[:reason]
|
|
216
|
+
elsif result[:passed]
|
|
217
|
+
expect(result[:passed]).to be(true)
|
|
218
|
+
else
|
|
219
|
+
# If schema parsed/validated successfully after external ref resolution,
|
|
220
|
+
# it means the external refs were resolvable (test passes after resolution)
|
|
221
|
+
error_msg = result[:error] || ''
|
|
222
|
+
if error_msg.include?('Expected validation error but schema') ||
|
|
223
|
+
error_msg.include?('parsed successfully')
|
|
224
|
+
skip 'Schema was incorrect due to unresolvable refs, but resolution now works'
|
|
225
|
+
else
|
|
226
|
+
raise "Test failed: #{result[:error]}\nSchema:\n#{schema_xml}"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
138
229
|
end
|
|
139
230
|
end
|
|
140
231
|
|
|
141
|
-
#
|
|
142
|
-
|
|
143
|
-
xml_content = invalid_xml.inner_text.strip
|
|
144
|
-
|
|
145
|
-
# Skip empty XML
|
|
146
|
-
next if xml_content.empty?
|
|
147
|
-
|
|
148
|
-
it "#{test_desc} - invalid XML example ##{index + 1}" do
|
|
149
|
-
skip "XML validation not yet implemented"
|
|
150
|
-
# Once validation is implemented, uncomment:
|
|
151
|
-
# schema = Rng::Grammar.from_xml(last_correct_schema_xml)
|
|
152
|
-
# expect(schema.valid?(xml_content)).to be false
|
|
153
|
-
end
|
|
154
|
-
end
|
|
232
|
+
# XML instance validation (valid/invalid) requires a full RELAX NG
|
|
233
|
+
# validator implementation - out of scope for schema parsing library
|
|
155
234
|
end
|
|
156
235
|
end
|
|
157
236
|
|
|
158
237
|
# Process nested test suites recursively
|
|
159
|
-
suite_element.xpath(
|
|
160
|
-
process_test_suite(nested_suite, context_desc)
|
|
238
|
+
suite_element.xpath('./testSuite').each do |nested_suite|
|
|
239
|
+
test_case_counter = process_test_suite(nested_suite, context_desc, test_case_counter: test_case_counter)
|
|
161
240
|
end
|
|
162
241
|
end
|
|
242
|
+
|
|
243
|
+
test_case_counter
|
|
163
244
|
end
|
|
164
245
|
|
|
165
|
-
RSpec.describe
|
|
246
|
+
RSpec.describe 'RELAX NG Specification Tests' do
|
|
166
247
|
# First, confirm the test file exists
|
|
167
|
-
it
|
|
248
|
+
it 'finds the spectest.xml file' do
|
|
168
249
|
expect(File.exist?(SPEC_TEST_XML_PATH)).to be true
|
|
169
250
|
end
|
|
170
251
|
|
|
171
252
|
# Generate a summary of the test suite
|
|
172
|
-
describe
|
|
173
|
-
it
|
|
174
|
-
sections =
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
correct_count =
|
|
178
|
-
incorrect_count =
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
puts "- #{incorrect_count} incorrect schemas"
|
|
185
|
-
puts "- #{valid_count} valid XML examples"
|
|
186
|
-
puts "- #{invalid_count} invalid XML examples"
|
|
253
|
+
describe 'Test Suite Summary' do
|
|
254
|
+
it 'contains test cases organized by section' do
|
|
255
|
+
sections = SPECTEST_XML.xpath('//section').map(&:text).uniq
|
|
256
|
+
expect(sections.length).to be > 0
|
|
257
|
+
|
|
258
|
+
correct_count = SPECTEST_XML.xpath('//correct').count
|
|
259
|
+
incorrect_count = SPECTEST_XML.xpath('//incorrect').count
|
|
260
|
+
SPECTEST_XML.xpath('//valid').count
|
|
261
|
+
SPECTEST_XML.xpath('//invalid').count
|
|
262
|
+
|
|
263
|
+
# Count test cases with resources
|
|
264
|
+
SPECTEST_XML.xpath('//testCase[resource]').count
|
|
187
265
|
|
|
188
266
|
total = correct_count + incorrect_count
|
|
189
267
|
expect(total).to be > 0
|
|
@@ -191,5 +269,5 @@ RSpec.describe "RELAX NG Specification Tests" do
|
|
|
191
269
|
end
|
|
192
270
|
|
|
193
271
|
# Start processing from the root test suite
|
|
194
|
-
process_test_suite(
|
|
272
|
+
process_test_suite(SPECTEST_XML.xpath('/testSuite').first)
|
|
195
273
|
end
|
data/spec/rng_spec.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require "equivalent-xml"
|
|
6
|
-
require "nokogiri"
|
|
7
|
-
require "diffy"
|
|
3
|
+
require 'rng'
|
|
4
|
+
require 'canon'
|
|
8
5
|
|
|
9
6
|
RSpec.configure do |config|
|
|
10
7
|
# Enable flags like --only-failures and --next-failure
|
|
11
|
-
config.example_status_persistence_file_path =
|
|
8
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
|
12
9
|
|
|
13
10
|
# Disable RSpec exposing methods globally on `Module` and `main`
|
|
14
11
|
config.disable_monkey_patching!
|
|
@@ -16,41 +13,9 @@ RSpec.configure do |config|
|
|
|
16
13
|
config.expect_with :rspec do |c|
|
|
17
14
|
c.syntax = :expect
|
|
18
15
|
end
|
|
16
|
+
end
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Xml::C14n.format(xml)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def format_xml(xml_string)
|
|
27
|
-
# Use Nokogiri to parse and format the XML
|
|
28
|
-
# Strip comments for comparison purposes - we care about structure, not documentation
|
|
29
|
-
doc = Nokogiri::XML(xml_string, &:noblanks)
|
|
30
|
-
|
|
31
|
-
# Remove all comment nodes before comparison
|
|
32
|
-
doc.xpath("//comment()").remove
|
|
33
|
-
|
|
34
|
-
doc.to_xml(indent: 2)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def diff_xml(actual, expected)
|
|
38
|
-
# Generate a readable diff between the formatted XML strings
|
|
39
|
-
Diffy::Diff.new(expected, actual, context: 3).to_s
|
|
40
|
-
end
|
|
41
|
-
end)
|
|
42
|
-
|
|
43
|
-
# Add custom matchers for XML comparison
|
|
44
|
-
RSpec::Matchers.define :be_equivalent_to_xml do |expected|
|
|
45
|
-
match do |actual|
|
|
46
|
-
@actual_formatted = format_xml(actual.to_s)
|
|
47
|
-
@expected_formatted = format_xml(expected.to_s)
|
|
48
|
-
EquivalentXml.equivalent?(@actual_formatted, @expected_formatted)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
failure_message do
|
|
52
|
-
diff = diff_xml(@actual_formatted, @expected_formatted)
|
|
53
|
-
"Expected XML to be equivalent, but it wasn't.\n\nDiff:\n#{diff}"
|
|
54
|
-
end
|
|
55
|
-
end
|
|
18
|
+
require 'lutaml/model'
|
|
19
|
+
Lutaml::Model::Config.configure do |config|
|
|
20
|
+
config.xml_adapter_type = :nokogiri
|
|
56
21
|
end
|