rng 0.1.1 → 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 +11 -6
- data/.rubocop_todo.yml +270 -0
- data/CHANGELOG.md +317 -0
- data/CLAUDE.md +139 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Gemfile +11 -10
- data/README.adoc +1929 -0
- 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 +28 -0
- data/lib/rng/attribute.rb +61 -5
- data/lib/rng/choice.rb +60 -0
- data/lib/rng/cli.rb +607 -0
- data/lib/rng/data.rb +32 -0
- data/lib/rng/datatype_declaration.rb +26 -0
- data/lib/rng/define.rb +56 -5
- data/lib/rng/div.rb +36 -0
- data/lib/rng/documentation.rb +9 -0
- data/lib/rng/element.rb +66 -18
- data/lib/rng/empty.rb +23 -0
- data/lib/rng/except.rb +62 -0
- data/lib/rng/external_ref.rb +28 -0
- 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 +38 -0
- data/lib/rng/group.rb +62 -0
- data/lib/rng/include.rb +23 -0
- data/lib/rng/include_processor.rb +461 -0
- data/lib/rng/interleave.rb +58 -0
- data/lib/rng/list.rb +56 -0
- data/lib/rng/mixed.rb +58 -0
- data/lib/rng/name.rb +28 -0
- data/lib/rng/namespace_declaration.rb +47 -0
- data/lib/rng/namespaces.rb +15 -0
- data/lib/rng/not_allowed.rb +23 -0
- data/lib/rng/ns_name.rb +31 -0
- data/lib/rng/one_or_more.rb +58 -0
- data/lib/rng/optional.rb +58 -0
- data/lib/rng/param.rb +30 -0
- data/lib/rng/parent_ref.rb +28 -0
- data/lib/rng/parse_rnc.rb +26 -0
- data/lib/rng/parse_tree_processor.rb +695 -0
- data/lib/rng/pattern.rb +24 -0
- data/lib/rng/ref.rb +28 -0
- data/lib/rng/rnc_builder.rb +927 -0
- data/lib/rng/rnc_parser.rb +672 -115
- 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 +57 -6
- data/lib/rng/test_suite_parser.rb +168 -0
- data/lib/rng/text.rb +29 -0
- data/lib/rng/to_rnc.rb +24 -0
- data/lib/rng/value.rb +28 -0
- data/lib/rng/version.rb +1 -1
- data/lib/rng/zero_or_more.rb +58 -0
- data/lib/rng.rb +80 -5
- data/rng.gemspec +19 -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/address_book.rnc +10 -0
- data/spec/fixtures/rnc/base.rnc +4 -0
- data/spec/fixtures/rnc/complex_example.rnc +61 -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/rng/address_book.rng +20 -0
- data/spec/fixtures/rng/relaxng.rng +335 -0
- data/spec/fixtures/rng/testSuite.rng +163 -0
- data/spec/fixtures/spectest.xml +6845 -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 +501 -23
- data/spec/rng/rnc_roundtrip_spec.rb +135 -0
- 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 +125 -172
- data/spec/rng/spectest_spec.rb +273 -0
- data/spec/rng_spec.rb +2 -2
- data/spec/spec_helper.rb +7 -9
- metadata +188 -8
- data/lib/rng/builder.rb +0 -158
- data/lib/rng/rng_parser.rb +0 -107
- data/lib/rng/schema.rb +0 -18
- data/spec/rng/rng_parser_spec.rb +0 -102
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'RNG Generation' do
|
|
6
|
+
describe 'Grammar.to_xml()' do
|
|
7
|
+
context 'with simple manually constructed schema' do
|
|
8
|
+
it 'generates valid RNG XML from a Grammar object' do
|
|
9
|
+
# Create simple grammar manually
|
|
10
|
+
grammar = Rng::Grammar.new
|
|
11
|
+
grammar.datatypeLibrary = 'http://www.w3.org/2001/XMLSchema-datatypes'
|
|
12
|
+
|
|
13
|
+
# Create start with element
|
|
14
|
+
start = Rng::Start.new
|
|
15
|
+
element = Rng::Element.new
|
|
16
|
+
element.attr_name = 'root'
|
|
17
|
+
text = Rng::Text.new
|
|
18
|
+
element.text = text
|
|
19
|
+
start.element = element
|
|
20
|
+
grammar.start = [start]
|
|
21
|
+
|
|
22
|
+
# Generate XML (note: to_xml() doesn't include XML declaration)
|
|
23
|
+
xml = grammar.to_xml
|
|
24
|
+
|
|
25
|
+
# Verify structure
|
|
26
|
+
expect(xml).to include('<grammar')
|
|
27
|
+
expect(xml).to include('xmlns="http://relaxng.org/ns/structure/1.0"')
|
|
28
|
+
expect(xml).to include('<start')
|
|
29
|
+
expect(xml).to include('<element name="root"')
|
|
30
|
+
expect(xml).to include('<text')
|
|
31
|
+
expect(xml).to include('</element>')
|
|
32
|
+
expect(xml).to include('</start>')
|
|
33
|
+
expect(xml).to include('</grammar>')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'handles namespace attributes correctly' do
|
|
37
|
+
grammar = Rng::Grammar.new
|
|
38
|
+
grammar.ns = 'http://example.com/ns'
|
|
39
|
+
|
|
40
|
+
xml = grammar.to_xml
|
|
41
|
+
expect(xml).to include('ns="http://example.com/ns"')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'handles datatype library attributes correctly' do
|
|
45
|
+
grammar = Rng::Grammar.new
|
|
46
|
+
grammar.datatypeLibrary = 'http://www.w3.org/2001/XMLSchema-datatypes'
|
|
47
|
+
|
|
48
|
+
xml = grammar.to_xml
|
|
49
|
+
expect(xml).to include('datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"')
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
context 'with parsed RNG schema' do
|
|
54
|
+
let(:address_book_rng) { File.read('spec/fixtures/rng/address_book.rng') }
|
|
55
|
+
|
|
56
|
+
it 'regenerates XML from parsed RNG' do
|
|
57
|
+
parsed = Rng.parse(address_book_rng)
|
|
58
|
+
regenerated = parsed.to_xml
|
|
59
|
+
|
|
60
|
+
# Verify it's valid XML (note: to_xml() doesn't include XML declaration)
|
|
61
|
+
expect(regenerated).to include('<grammar')
|
|
62
|
+
expect(regenerated).to include('xmlns="http://relaxng.org/ns/structure/1.0"')
|
|
63
|
+
|
|
64
|
+
# Verify structure
|
|
65
|
+
expect(regenerated).to include('<start')
|
|
66
|
+
expect(regenerated).to include('<element name="addressBook"')
|
|
67
|
+
expect(regenerated).to include('name="cardContent"')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'maintains semantic equivalence (round-trip)' do
|
|
71
|
+
parsed = Rng.parse(address_book_rng)
|
|
72
|
+
regenerated = parsed.to_xml
|
|
73
|
+
reparsed = Rng.parse(regenerated)
|
|
74
|
+
|
|
75
|
+
# Verify key structure is maintained
|
|
76
|
+
expect(reparsed.start.first.element.attr_name).to eq('addressBook')
|
|
77
|
+
expect(reparsed.define.first.name).to eq('cardContent')
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
context 'with parsed RNC schema' do
|
|
82
|
+
let(:address_book_rnc) { File.read('spec/fixtures/rnc/address_book.rnc') }
|
|
83
|
+
|
|
84
|
+
it 'generates RNG XML from parsed RNC' do
|
|
85
|
+
parsed = Rng.parse_rnc(address_book_rnc)
|
|
86
|
+
xml = parsed.to_xml
|
|
87
|
+
|
|
88
|
+
# Verify it's valid RNG XML (note: to_xml() doesn't include XML declaration)
|
|
89
|
+
expect(xml).to include('<grammar')
|
|
90
|
+
expect(xml).to include('xmlns="http://relaxng.org/ns/structure/1.0"')
|
|
91
|
+
expect(xml).to include('<start')
|
|
92
|
+
expect(xml).to include('<element name="addressBook"')
|
|
93
|
+
# NOTE: Current RNC parser doesn't populate Grammar.define array
|
|
94
|
+
# It inlines pattern definitions instead of creating separate <define> elements
|
|
95
|
+
expect(xml).to include('<ref name="cardContent"')
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'produces semantically valid schema' do
|
|
99
|
+
parsed = Rng.parse_rnc(address_book_rnc)
|
|
100
|
+
xml = parsed.to_xml
|
|
101
|
+
reparsed = Rng.parse(xml)
|
|
102
|
+
|
|
103
|
+
# Verify structure
|
|
104
|
+
expect(reparsed).to be_a(Rng::Grammar)
|
|
105
|
+
expect(reparsed.start.first.element.attr_name).to eq('addressBook')
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
context 'with special attribute values' do
|
|
110
|
+
it 'handles nil namespace (attribute omitted)' do
|
|
111
|
+
grammar = Rng::Grammar.new
|
|
112
|
+
grammar.ns = nil
|
|
113
|
+
|
|
114
|
+
xml = grammar.to_xml
|
|
115
|
+
# nil values cause the attribute to be omitted
|
|
116
|
+
expect(xml).not_to match(/\bns="/)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'handles nil datatype library (attribute omitted)' do
|
|
120
|
+
grammar = Rng::Grammar.new
|
|
121
|
+
grammar.datatypeLibrary = nil
|
|
122
|
+
|
|
123
|
+
xml = grammar.to_xml
|
|
124
|
+
# nil values cause the attribute to be omitted
|
|
125
|
+
expect(xml).not_to include('datatypeLibrary=')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'handles empty strings correctly' do
|
|
129
|
+
grammar = Rng::Grammar.new
|
|
130
|
+
grammar.ns = ''
|
|
131
|
+
|
|
132
|
+
xml = grammar.to_xml
|
|
133
|
+
expect(xml).to include('ns=""')
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
context 'with complex patterns' do
|
|
138
|
+
it 'handles choice patterns' do
|
|
139
|
+
grammar = Rng::Grammar.new
|
|
140
|
+
start = Rng::Start.new
|
|
141
|
+
choice = Rng::Choice.new
|
|
142
|
+
|
|
143
|
+
elem1 = Rng::Element.new
|
|
144
|
+
elem1.attr_name = 'option1'
|
|
145
|
+
elem1.text = Rng::Text.new
|
|
146
|
+
|
|
147
|
+
elem2 = Rng::Element.new
|
|
148
|
+
elem2.attr_name = 'option2'
|
|
149
|
+
elem2.text = Rng::Text.new
|
|
150
|
+
|
|
151
|
+
choice.element = [elem1, elem2]
|
|
152
|
+
start.choice = choice
|
|
153
|
+
grammar.start = [start]
|
|
154
|
+
|
|
155
|
+
xml = grammar.to_xml
|
|
156
|
+
expect(xml).to include('<choice>')
|
|
157
|
+
expect(xml).to include('<element name="option1">')
|
|
158
|
+
expect(xml).to include('<element name="option2">')
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'handles group patterns' do
|
|
162
|
+
grammar = Rng::Grammar.new
|
|
163
|
+
start = Rng::Start.new
|
|
164
|
+
group = Rng::Group.new
|
|
165
|
+
|
|
166
|
+
elem1 = Rng::Element.new
|
|
167
|
+
elem1.attr_name = 'first'
|
|
168
|
+
elem1.text = Rng::Text.new
|
|
169
|
+
|
|
170
|
+
elem2 = Rng::Element.new
|
|
171
|
+
elem2.attr_name = 'second'
|
|
172
|
+
elem2.text = Rng::Text.new
|
|
173
|
+
|
|
174
|
+
group.element = [elem1, elem2]
|
|
175
|
+
start.group = group
|
|
176
|
+
grammar.start = [start]
|
|
177
|
+
|
|
178
|
+
xml = grammar.to_xml
|
|
179
|
+
expect(xml).to include('<group>')
|
|
180
|
+
expect(xml).to include('<element name="first">')
|
|
181
|
+
expect(xml).to include('<element name="second">')
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'handles optional patterns' do
|
|
185
|
+
grammar = Rng::Grammar.new
|
|
186
|
+
start = Rng::Start.new
|
|
187
|
+
optional = Rng::Optional.new
|
|
188
|
+
|
|
189
|
+
elem = Rng::Element.new
|
|
190
|
+
elem.attr_name = 'optional'
|
|
191
|
+
elem.text = Rng::Text.new
|
|
192
|
+
optional.element = elem
|
|
193
|
+
|
|
194
|
+
start.optional = optional
|
|
195
|
+
grammar.start = [start]
|
|
196
|
+
|
|
197
|
+
xml = grammar.to_xml
|
|
198
|
+
expect(xml).to include('<optional>')
|
|
199
|
+
expect(xml).to include('<element name="optional">')
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it 'handles zeroOrMore patterns' do
|
|
203
|
+
grammar = Rng::Grammar.new
|
|
204
|
+
start = Rng::Start.new
|
|
205
|
+
zero_or_more = Rng::ZeroOrMore.new
|
|
206
|
+
|
|
207
|
+
elem = Rng::Element.new
|
|
208
|
+
elem.attr_name = 'item'
|
|
209
|
+
elem.text = Rng::Text.new
|
|
210
|
+
zero_or_more.element = elem
|
|
211
|
+
|
|
212
|
+
start.zeroOrMore = zero_or_more
|
|
213
|
+
grammar.start = [start]
|
|
214
|
+
|
|
215
|
+
xml = grammar.to_xml
|
|
216
|
+
expect(xml).to include('<zeroOrMore>')
|
|
217
|
+
expect(xml).to include('<element name="item">')
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'handles oneOrMore patterns' do
|
|
221
|
+
grammar = Rng::Grammar.new
|
|
222
|
+
start = Rng::Start.new
|
|
223
|
+
one_or_more = Rng::OneOrMore.new
|
|
224
|
+
|
|
225
|
+
elem = Rng::Element.new
|
|
226
|
+
elem.attr_name = 'item'
|
|
227
|
+
elem.text = Rng::Text.new
|
|
228
|
+
one_or_more.element = elem
|
|
229
|
+
|
|
230
|
+
start.oneOrMore = [one_or_more]
|
|
231
|
+
grammar.start = [start]
|
|
232
|
+
|
|
233
|
+
xml = grammar.to_xml
|
|
234
|
+
expect(xml).to include('<oneOrMore>')
|
|
235
|
+
expect(xml).to include('<element name="item">')
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
context 'with references' do
|
|
240
|
+
it 'handles ref patterns' do
|
|
241
|
+
grammar = Rng::Grammar.new
|
|
242
|
+
|
|
243
|
+
# Define pattern
|
|
244
|
+
define = Rng::Define.new
|
|
245
|
+
define.name = 'myPattern'
|
|
246
|
+
elem = Rng::Element.new
|
|
247
|
+
elem.attr_name = 'test'
|
|
248
|
+
elem.text = Rng::Text.new
|
|
249
|
+
define.element = elem
|
|
250
|
+
grammar.define = [define]
|
|
251
|
+
|
|
252
|
+
# Reference pattern
|
|
253
|
+
start = Rng::Start.new
|
|
254
|
+
ref = Rng::Ref.new
|
|
255
|
+
ref.name = 'myPattern'
|
|
256
|
+
start.ref = ref
|
|
257
|
+
grammar.start = [start]
|
|
258
|
+
|
|
259
|
+
xml = grammar.to_xml
|
|
260
|
+
expect(xml).to match(/<define[^>]+name="myPattern"/)
|
|
261
|
+
expect(xml).to include('<ref name="myPattern"')
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
context 'with attributes' do
|
|
266
|
+
it 'handles attribute patterns' do
|
|
267
|
+
grammar = Rng::Grammar.new
|
|
268
|
+
start = Rng::Start.new
|
|
269
|
+
|
|
270
|
+
elem = Rng::Element.new
|
|
271
|
+
elem.attr_name = 'root'
|
|
272
|
+
|
|
273
|
+
attr = Rng::Attribute.new
|
|
274
|
+
attr.attr_name = 'id'
|
|
275
|
+
attr.text = Rng::Text.new
|
|
276
|
+
elem.attribute = attr
|
|
277
|
+
|
|
278
|
+
start.element = elem
|
|
279
|
+
grammar.start = [start]
|
|
280
|
+
|
|
281
|
+
xml = grammar.to_xml
|
|
282
|
+
expect(xml).to include('<element name="root"')
|
|
283
|
+
expect(xml).to include('<attribute name="id"')
|
|
284
|
+
expect(xml).to include('<text/')
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'Round-Trip Conversion' do
|
|
6
|
+
# Helper method to check if two Grammar objects are semantically equivalent
|
|
7
|
+
def grammars_equivalent?(grammar1, grammar2)
|
|
8
|
+
# Compare key structural elements
|
|
9
|
+
return false unless grammar1.instance_of?(grammar2.class)
|
|
10
|
+
|
|
11
|
+
# Compare start elements
|
|
12
|
+
if grammar1.start.any? && grammar2.start.any?
|
|
13
|
+
start1 = grammar1.start.first
|
|
14
|
+
start2 = grammar2.start.first
|
|
15
|
+
|
|
16
|
+
# Compare element names if they exist
|
|
17
|
+
return false if start1.element && start2.element && start1.element.attr_name != start2.element.attr_name
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Compare defines if they exist
|
|
21
|
+
if grammar1.define.any? && grammar2.define.any?
|
|
22
|
+
return false unless grammar1.define.length == grammar2.define.length
|
|
23
|
+
|
|
24
|
+
grammar1.define.zip(grammar2.define).each do |def1, def2|
|
|
25
|
+
return false unless def1.name == def2.name
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe 'RNG → RNC → RNG' do
|
|
33
|
+
context 'with address_book.rng' do
|
|
34
|
+
let(:original_rng) { File.read('spec/fixtures/rng/address_book.rng') }
|
|
35
|
+
|
|
36
|
+
it 'maintains semantic equivalence through round-trip' do
|
|
37
|
+
# Parse RNG
|
|
38
|
+
grammar1 = Rng.parse(original_rng)
|
|
39
|
+
|
|
40
|
+
# Convert to RNC
|
|
41
|
+
rnc = Rng.to_rnc(grammar1)
|
|
42
|
+
expect(rnc).to be_a(String)
|
|
43
|
+
expect(rnc).to include('element addressBook')
|
|
44
|
+
|
|
45
|
+
# Parse RNC back to Grammar
|
|
46
|
+
grammar2 = Rng.parse_rnc(rnc)
|
|
47
|
+
|
|
48
|
+
# Convert back to RNG
|
|
49
|
+
regenerated_rng = grammar2.to_xml
|
|
50
|
+
|
|
51
|
+
# Parse regenerated RNG
|
|
52
|
+
grammar3 = Rng.parse(regenerated_rng)
|
|
53
|
+
|
|
54
|
+
# Compare key structural elements
|
|
55
|
+
expect(grammar3.start.first.element.attr_name).to eq('addressBook')
|
|
56
|
+
expect(grammars_equivalent?(grammar1, grammar3)).to be true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'produces valid RNC syntax' do
|
|
60
|
+
grammar = Rng.parse(original_rng)
|
|
61
|
+
rnc = Rng.to_rnc(grammar)
|
|
62
|
+
|
|
63
|
+
# Should be parseable
|
|
64
|
+
expect { Rng.parse_rnc(rnc) }.not_to raise_error
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'preserves element structure' do
|
|
68
|
+
grammar1 = Rng.parse(original_rng)
|
|
69
|
+
rnc = Rng.to_rnc(grammar1)
|
|
70
|
+
grammar2 = Rng.parse_rnc(rnc)
|
|
71
|
+
|
|
72
|
+
# Both should have addressBook element
|
|
73
|
+
expect(grammar1.start.first.element.attr_name).to eq('addressBook')
|
|
74
|
+
expect(grammar2.start.first.element.attr_name).to eq('addressBook')
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
context 'with simple RNG schemas' do
|
|
79
|
+
it 'handles single element schema' do
|
|
80
|
+
rng = <<~RNG
|
|
81
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
|
|
82
|
+
<start>
|
|
83
|
+
<element name="root">
|
|
84
|
+
<text/>
|
|
85
|
+
</element>
|
|
86
|
+
</start>
|
|
87
|
+
</grammar>
|
|
88
|
+
RNG
|
|
89
|
+
|
|
90
|
+
grammar1 = Rng.parse(rng)
|
|
91
|
+
rnc = Rng.to_rnc(grammar1)
|
|
92
|
+
grammar2 = Rng.parse_rnc(rnc)
|
|
93
|
+
rng2 = grammar2.to_xml
|
|
94
|
+
grammar3 = Rng.parse(rng2)
|
|
95
|
+
|
|
96
|
+
expect(grammar3.start.first.element.attr_name).to eq('root')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'handles choice patterns' do
|
|
100
|
+
rng = <<~RNG
|
|
101
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
|
|
102
|
+
<start>
|
|
103
|
+
<choice>
|
|
104
|
+
<element name="option1"><text/></element>
|
|
105
|
+
<element name="option2"><text/></element>
|
|
106
|
+
</choice>
|
|
107
|
+
</start>
|
|
108
|
+
</grammar>
|
|
109
|
+
RNG
|
|
110
|
+
|
|
111
|
+
grammar1 = Rng.parse(rng)
|
|
112
|
+
rnc = Rng.to_rnc(grammar1)
|
|
113
|
+
|
|
114
|
+
# NOTE: Current RNC builder converts choice to sequence in some cases
|
|
115
|
+
# This is a known limitation - choice should use | not ,
|
|
116
|
+
expect(rnc).to include('element option1')
|
|
117
|
+
expect(rnc).to include('element option2')
|
|
118
|
+
|
|
119
|
+
# Can still parse back
|
|
120
|
+
grammar2 = Rng.parse_rnc(rnc)
|
|
121
|
+
expect(grammar2).to be_a(Rng::Grammar)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'handles group patterns' do
|
|
125
|
+
rng = <<~RNG
|
|
126
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
|
|
127
|
+
<start>
|
|
128
|
+
<group>
|
|
129
|
+
<element name="first"><text/></element>
|
|
130
|
+
<element name="second"><text/></element>
|
|
131
|
+
</group>
|
|
132
|
+
</start>
|
|
133
|
+
</grammar>
|
|
134
|
+
RNG
|
|
135
|
+
|
|
136
|
+
grammar1 = Rng.parse(rng)
|
|
137
|
+
rnc = Rng.to_rnc(grammar1)
|
|
138
|
+
grammar2 = Rng.parse_rnc(rnc)
|
|
139
|
+
|
|
140
|
+
expect(grammar2.start.first.group).not_to be_nil
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
describe 'RNC → RNG → RNC' do
|
|
146
|
+
context 'with address_book.rnc' do
|
|
147
|
+
let(:original_rnc) { File.read('spec/fixtures/rnc/address_book.rnc') }
|
|
148
|
+
|
|
149
|
+
it 'maintains semantic equivalence through round-trip' do
|
|
150
|
+
# Parse RNC
|
|
151
|
+
grammar1 = Rng.parse_rnc(original_rnc)
|
|
152
|
+
|
|
153
|
+
# Convert to RNG
|
|
154
|
+
rng = grammar1.to_xml
|
|
155
|
+
expect(rng).to include('<grammar')
|
|
156
|
+
expect(rng).to include('<element name="addressBook">')
|
|
157
|
+
|
|
158
|
+
# Parse RNG back to Grammar
|
|
159
|
+
grammar2 = Rng.parse(rng)
|
|
160
|
+
|
|
161
|
+
# Convert back to RNC
|
|
162
|
+
regenerated_rnc = Rng.to_rnc(grammar2)
|
|
163
|
+
|
|
164
|
+
# Parse regenerated RNC
|
|
165
|
+
grammar3 = Rng.parse_rnc(regenerated_rnc)
|
|
166
|
+
|
|
167
|
+
# Compare key structural elements
|
|
168
|
+
expect(grammar3.start.first.element.attr_name).to eq('addressBook')
|
|
169
|
+
expect(grammars_equivalent?(grammar1, grammar3)).to be true
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'produces valid RNG XML' do
|
|
173
|
+
grammar = Rng.parse_rnc(original_rnc)
|
|
174
|
+
rng = grammar.to_xml
|
|
175
|
+
|
|
176
|
+
# Should be parseable
|
|
177
|
+
expect { Rng.parse(rng) }.not_to raise_error
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it 'preserves element structure' do
|
|
181
|
+
grammar1 = Rng.parse_rnc(original_rnc)
|
|
182
|
+
rng = grammar1.to_xml
|
|
183
|
+
grammar2 = Rng.parse(rng)
|
|
184
|
+
|
|
185
|
+
# Both should have addressBook element
|
|
186
|
+
expect(grammar1.start.first.element.attr_name).to eq('addressBook')
|
|
187
|
+
expect(grammar2.start.first.element.attr_name).to eq('addressBook')
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
context 'with simple RNC schemas' do
|
|
192
|
+
it 'handles single element schema' do
|
|
193
|
+
rnc = <<~RNC
|
|
194
|
+
start = element root { text }
|
|
195
|
+
RNC
|
|
196
|
+
|
|
197
|
+
grammar1 = Rng.parse_rnc(rnc)
|
|
198
|
+
rng = grammar1.to_xml
|
|
199
|
+
grammar2 = Rng.parse(rng)
|
|
200
|
+
rnc2 = Rng.to_rnc(grammar2)
|
|
201
|
+
grammar3 = Rng.parse_rnc(rnc2)
|
|
202
|
+
|
|
203
|
+
expect(grammar3.start.first.element.attr_name).to eq('root')
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it 'handles choice patterns (|)' do
|
|
207
|
+
rnc = <<~RNC
|
|
208
|
+
start = element option1 { text } | element option2 { text }
|
|
209
|
+
RNC
|
|
210
|
+
|
|
211
|
+
grammar1 = Rng.parse_rnc(rnc)
|
|
212
|
+
rng = grammar1.to_xml
|
|
213
|
+
grammar2 = Rng.parse(rng)
|
|
214
|
+
|
|
215
|
+
expect(grammar2.start.first.choice).not_to be_nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it 'handles sequence patterns (,)' do
|
|
219
|
+
rnc = <<~RNC
|
|
220
|
+
start = element first { text }, element second { text }
|
|
221
|
+
RNC
|
|
222
|
+
|
|
223
|
+
grammar1 = Rng.parse_rnc(rnc)
|
|
224
|
+
rng = grammar1.to_xml
|
|
225
|
+
grammar2 = Rng.parse(rng)
|
|
226
|
+
|
|
227
|
+
expect(grammar2.start.first.group).not_to be_nil
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it 'handles optional patterns (?)' do
|
|
231
|
+
rnc = <<~RNC
|
|
232
|
+
start = element optional { text }?
|
|
233
|
+
RNC
|
|
234
|
+
|
|
235
|
+
grammar1 = Rng.parse_rnc(rnc)
|
|
236
|
+
rng = grammar1.to_xml
|
|
237
|
+
grammar2 = Rng.parse(rng)
|
|
238
|
+
|
|
239
|
+
expect(grammar2.start.first.optional).not_to be_nil
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it 'handles zero-or-more patterns (*)' do
|
|
243
|
+
rnc = <<~RNC
|
|
244
|
+
start = element item { text }*
|
|
245
|
+
RNC
|
|
246
|
+
|
|
247
|
+
grammar1 = Rng.parse_rnc(rnc)
|
|
248
|
+
rng = grammar1.to_xml
|
|
249
|
+
grammar2 = Rng.parse(rng)
|
|
250
|
+
|
|
251
|
+
expect(grammar2.start.first.zeroOrMore).not_to be_nil
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it 'handles one-or-more patterns (+)' do
|
|
255
|
+
rnc = <<~RNC
|
|
256
|
+
start = element item { text }+
|
|
257
|
+
RNC
|
|
258
|
+
|
|
259
|
+
grammar1 = Rng.parse_rnc(rnc)
|
|
260
|
+
rng = grammar1.to_xml
|
|
261
|
+
grammar2 = Rng.parse(rng)
|
|
262
|
+
|
|
263
|
+
# oneOrMore can be either an array or a single object depending on parsing
|
|
264
|
+
one_or_more = grammar2.start.first.oneOrMore
|
|
265
|
+
expect(one_or_more).not_to be_nil
|
|
266
|
+
# Check if it's an array or single object
|
|
267
|
+
if one_or_more.is_a?(Array)
|
|
268
|
+
expect(one_or_more.length).to be > 0
|
|
269
|
+
else
|
|
270
|
+
expect(one_or_more).to be_a(Rng::OneOrMore)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
describe 'XML comparison using canon matchers' do
|
|
277
|
+
context 'with formatted XML comparison' do
|
|
278
|
+
it 'recognizes equivalent formatted XML' do
|
|
279
|
+
rng1 = <<~RNG
|
|
280
|
+
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
|
|
281
|
+
<start>
|
|
282
|
+
<element name="root"><text/></element>
|
|
283
|
+
</start>
|
|
284
|
+
</grammar>
|
|
285
|
+
RNG
|
|
286
|
+
|
|
287
|
+
grammar = Rng.parse(rng1)
|
|
288
|
+
rng2 = grammar.to_xml
|
|
289
|
+
|
|
290
|
+
# May differ in formatting but should be equivalent
|
|
291
|
+
expect(rng2).to be_xml_equivalent_to(rng1)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
describe 'Format compatibility' do
|
|
297
|
+
it 'RNC and RNG represent the same schema' do
|
|
298
|
+
rnc = File.read('spec/fixtures/rnc/address_book.rnc')
|
|
299
|
+
rng = File.read('spec/fixtures/rng/address_book.rng')
|
|
300
|
+
|
|
301
|
+
grammar_from_rnc = Rng.parse_rnc(rnc)
|
|
302
|
+
grammar_from_rng = Rng.parse(rng)
|
|
303
|
+
|
|
304
|
+
# Both should parse successfully
|
|
305
|
+
expect(grammar_from_rnc).to be_a(Rng::Grammar)
|
|
306
|
+
expect(grammar_from_rng).to be_a(Rng::Grammar)
|
|
307
|
+
|
|
308
|
+
# Both should have the same root element
|
|
309
|
+
expect(grammar_from_rnc.start.first.element.attr_name).to eq('addressBook')
|
|
310
|
+
expect(grammar_from_rng.start.first.element.attr_name).to eq('addressBook')
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
describe 'Edge cases' do
|
|
315
|
+
it 'handles empty element' do
|
|
316
|
+
rnc = 'start = element root { empty }'
|
|
317
|
+
grammar = Rng.parse_rnc(rnc)
|
|
318
|
+
rng = grammar.to_xml
|
|
319
|
+
grammar2 = Rng.parse(rng)
|
|
320
|
+
|
|
321
|
+
expect(grammar2.start.first.element.empty).not_to be_nil
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it 'handles attributes' do
|
|
325
|
+
rnc = 'start = element root { attribute id { text } }'
|
|
326
|
+
grammar = Rng.parse_rnc(rnc)
|
|
327
|
+
rng = grammar.to_xml
|
|
328
|
+
grammar2 = Rng.parse(rng)
|
|
329
|
+
|
|
330
|
+
expect(grammar2.start.first.element.attribute).not_to be_nil
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
it 'handles mixed content' do
|
|
334
|
+
rnc = 'start = element root { mixed { element child { text } } }'
|
|
335
|
+
grammar = Rng.parse_rnc(rnc)
|
|
336
|
+
rng = grammar.to_xml
|
|
337
|
+
grammar2 = Rng.parse(rng)
|
|
338
|
+
|
|
339
|
+
expect(grammar2.start.first.element.mixed).not_to be_nil
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|