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/rnc_parser_spec.rb
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
RSpec.describe Rng::RncParser do
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
xdescribe "#parse" do
|
|
9
|
-
context "with a simple RNC schema" do
|
|
6
|
+
describe '#parse' do
|
|
7
|
+
context 'with a simple RNC schema' do
|
|
10
8
|
let(:input) do
|
|
11
9
|
<<~RNC
|
|
12
10
|
element addressBook {
|
|
@@ -18,16 +16,14 @@ RSpec.describe Rng::RncParser do
|
|
|
18
16
|
RNC
|
|
19
17
|
end
|
|
20
18
|
|
|
21
|
-
it
|
|
22
|
-
result =
|
|
19
|
+
it 'correctly parses the schema' do
|
|
20
|
+
result = described_class.parse(input)
|
|
23
21
|
expect(result).to be_a(Rng::Grammar)
|
|
24
|
-
expect(result.start.
|
|
25
|
-
expect(result.start.elements.first.elements.first.name).to eq("card")
|
|
26
|
-
expect(result.start.elements.first.elements.first.elements.map(&:name)).to eq(%w[name email])
|
|
22
|
+
expect(result.start.first.element.attr_name).to eq('addressBook')
|
|
27
23
|
end
|
|
28
24
|
end
|
|
29
25
|
|
|
30
|
-
context
|
|
26
|
+
context 'with attributes' do
|
|
31
27
|
let(:input) do
|
|
32
28
|
<<~RNC
|
|
33
29
|
element person {
|
|
@@ -37,15 +33,13 @@ RSpec.describe Rng::RncParser do
|
|
|
37
33
|
RNC
|
|
38
34
|
end
|
|
39
35
|
|
|
40
|
-
it
|
|
41
|
-
result =
|
|
42
|
-
expect(result.start.
|
|
43
|
-
expect(result.start.elements.first.elements.first).to be_a(Rng::Attribute)
|
|
44
|
-
expect(result.start.elements.first.elements.first.name).to eq("id")
|
|
36
|
+
it 'correctly parses attributes' do
|
|
37
|
+
result = described_class.parse(input)
|
|
38
|
+
expect(result.start.first.element.attr_name).to eq('person')
|
|
45
39
|
end
|
|
46
40
|
end
|
|
47
41
|
|
|
48
|
-
context
|
|
42
|
+
context 'with nested elements' do
|
|
49
43
|
let(:input) do
|
|
50
44
|
<<~RNC
|
|
51
45
|
element root {
|
|
@@ -57,11 +51,493 @@ RSpec.describe Rng::RncParser do
|
|
|
57
51
|
RNC
|
|
58
52
|
end
|
|
59
53
|
|
|
60
|
-
it
|
|
61
|
-
result =
|
|
62
|
-
expect(result.start.
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
it 'correctly parses nested elements' do
|
|
55
|
+
result = described_class.parse(input)
|
|
56
|
+
expect(result.start.first.element.attr_name).to eq('root')
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe '#to_rnc' do
|
|
62
|
+
context 'with address_book.rnc' do
|
|
63
|
+
let(:rnc_input) { File.read('spec/fixtures/rnc/address_book.rnc') }
|
|
64
|
+
|
|
65
|
+
it 'round-trips successfully' do
|
|
66
|
+
# Parse RNC to Grammar
|
|
67
|
+
grammar1 = described_class.parse(rnc_input)
|
|
68
|
+
|
|
69
|
+
# Generate RNC from Grammar
|
|
70
|
+
rnc_output = described_class.to_rnc(grammar1)
|
|
71
|
+
|
|
72
|
+
# Parse generated RNC back to Grammar
|
|
73
|
+
grammar2 = described_class.parse(rnc_output)
|
|
74
|
+
|
|
75
|
+
# Compare key properties
|
|
76
|
+
expect(grammar2.start.first.element.name).to eq(grammar1.start.first.element.name)
|
|
77
|
+
expect(grammar2.define.map(&:name)).to eq(grammar1.define.map(&:name))
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
context 'with value literals' do
|
|
82
|
+
let(:input) do
|
|
83
|
+
<<~RNC
|
|
84
|
+
start = element doc {
|
|
85
|
+
attribute version { "1.0" }
|
|
86
|
+
}
|
|
87
|
+
RNC
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'generates value literals correctly' do
|
|
91
|
+
grammar = described_class.parse(input)
|
|
92
|
+
rnc = described_class.to_rnc(grammar)
|
|
93
|
+
|
|
94
|
+
expect(rnc).to include('"1.0"')
|
|
95
|
+
expect(rnc).to include('attribute version')
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
context 'with choice of values' do
|
|
100
|
+
let(:input) do
|
|
101
|
+
<<~RNC
|
|
102
|
+
start = element admonition {
|
|
103
|
+
attribute type { "note" | "warning" | "tip" }
|
|
104
|
+
}
|
|
105
|
+
RNC
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'generates choice of values correctly' do
|
|
109
|
+
grammar = described_class.parse(input)
|
|
110
|
+
rnc = described_class.to_rnc(grammar)
|
|
111
|
+
|
|
112
|
+
expect(rnc).to include('"note"')
|
|
113
|
+
expect(rnc).to include('"warning"')
|
|
114
|
+
expect(rnc).to include('"tip"')
|
|
115
|
+
expect(rnc).to include('|')
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
context 'with namespace declaration' do
|
|
120
|
+
let(:input) do
|
|
121
|
+
<<~RNC
|
|
122
|
+
default namespace = "http://example.org/ns"
|
|
123
|
+
|
|
124
|
+
start = element root { text }
|
|
125
|
+
RNC
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'generates namespace declaration' do
|
|
129
|
+
grammar = described_class.parse(input)
|
|
130
|
+
rnc = described_class.to_rnc(grammar)
|
|
131
|
+
|
|
132
|
+
expect(rnc).to include('default namespace = "http://example.org/ns"')
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
context 'with mixed content' do
|
|
137
|
+
let(:input) do
|
|
138
|
+
<<~RNC
|
|
139
|
+
start = element para {
|
|
140
|
+
mixed {
|
|
141
|
+
element emphasis { text }*
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
RNC
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'generates mixed content correctly' do
|
|
148
|
+
grammar = described_class.parse(input)
|
|
149
|
+
rnc = described_class.to_rnc(grammar)
|
|
150
|
+
|
|
151
|
+
expect(rnc).to include('mixed {')
|
|
152
|
+
expect(rnc).to include('element emphasis')
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
context 'with datatype library' do
|
|
157
|
+
it 'generates datatype library declaration' do
|
|
158
|
+
# Create a Grammar object directly with datatype library
|
|
159
|
+
grammar = Rng::Grammar.new
|
|
160
|
+
grammar.datatypeLibrary = 'http://www.w3.org/2001/XMLSchema-datatypes'
|
|
161
|
+
|
|
162
|
+
# Create a simple start pattern
|
|
163
|
+
start = Rng::Start.new
|
|
164
|
+
element = Rng::Element.new
|
|
165
|
+
element.attr_name = 'person'
|
|
166
|
+
|
|
167
|
+
attribute = Rng::Attribute.new
|
|
168
|
+
attribute.attr_name = 'id'
|
|
169
|
+
|
|
170
|
+
data = Rng::Data.new
|
|
171
|
+
data.type = 'ID'
|
|
172
|
+
attribute.data = data
|
|
173
|
+
|
|
174
|
+
element.attribute = attribute
|
|
175
|
+
start.element = element
|
|
176
|
+
grammar.start = [start]
|
|
177
|
+
|
|
178
|
+
rnc = described_class.to_rnc(grammar)
|
|
179
|
+
|
|
180
|
+
expect(rnc).to include('datatypes xsd')
|
|
181
|
+
expect(rnc).to include('xsd:ID')
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
context 'with pattern references in choice' do
|
|
186
|
+
let(:input) do
|
|
187
|
+
<<~RNC
|
|
188
|
+
start = element doc {
|
|
189
|
+
(section1 | section2)+
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
section1 = element section1 { text }
|
|
193
|
+
section2 = element section2 { text }
|
|
194
|
+
RNC
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it 'parses choice of references correctly' do
|
|
198
|
+
grammar = described_class.parse(input)
|
|
199
|
+
|
|
200
|
+
# Should have start with oneOrMore containing choice of refs
|
|
201
|
+
expect(grammar.start).not_to be_empty
|
|
202
|
+
expect(grammar.define.length).to eq(2)
|
|
203
|
+
expect(grammar.define.map(&:name)).to contain_exactly('section1',
|
|
204
|
+
'section2')
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it 'round-trips choice of references correctly' do
|
|
208
|
+
grammar1 = described_class.parse(input)
|
|
209
|
+
rnc = described_class.to_rnc(grammar1)
|
|
210
|
+
grammar2 = described_class.parse(rnc)
|
|
211
|
+
|
|
212
|
+
expect(grammar2.define.map(&:name).sort).to eq(grammar1.define.map(&:name).sort)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Metanorma Schema Tests - Test all real-world schemas
|
|
218
|
+
describe 'Metanorma Schema Tests' do
|
|
219
|
+
# Get all RNC files from metanorma fixtures
|
|
220
|
+
rnc_files = Dir.glob('spec/fixtures/metanorma/*.rnc')
|
|
221
|
+
|
|
222
|
+
if rnc_files.empty?
|
|
223
|
+
it 'has Metanorma schema fixtures available' do
|
|
224
|
+
skip 'No Metanorma RNC files found in spec/fixtures/metanorma/'
|
|
225
|
+
end
|
|
226
|
+
else
|
|
227
|
+
rnc_files.each do |rnc_file|
|
|
228
|
+
schema_name = File.basename(rnc_file, '.rnc')
|
|
229
|
+
|
|
230
|
+
context "with #{schema_name}" do
|
|
231
|
+
it 'parses successfully' do
|
|
232
|
+
expect { Rng.parse_file(rnc_file) }.not_to raise_error
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it 'produces valid Grammar object' do
|
|
236
|
+
grammar = Rng.parse_file(rnc_file)
|
|
237
|
+
expect(grammar).to be_a(Rng::Grammar)
|
|
238
|
+
expect(grammar.start).not_to be_nil
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
it 'round-trips correctly' do
|
|
242
|
+
grammar1 = Rng.parse_file(rnc_file)
|
|
243
|
+
rnc_generated = described_class.to_rnc(grammar1)
|
|
244
|
+
grammar2 = described_class.parse(rnc_generated)
|
|
245
|
+
|
|
246
|
+
# Compare structure
|
|
247
|
+
expect(grammar2.start.first.element.name).to eq(grammar1.start.first.element.name) if grammar1.start&.first&.element&.name
|
|
248
|
+
|
|
249
|
+
# Compare defined patterns
|
|
250
|
+
expect(grammar2.define.map(&:name).sort).to eq(grammar1.define.map(&:name).sort) if grammar1.define && grammar2.define
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Complex RELAX NG Pattern Tests
|
|
258
|
+
describe 'Complex RELAX NG Patterns' do
|
|
259
|
+
context 'with interleave patterns' do
|
|
260
|
+
let(:input) do
|
|
261
|
+
<<~RNC
|
|
262
|
+
start = element doc {
|
|
263
|
+
element a { text } &
|
|
264
|
+
element b { text }
|
|
265
|
+
}
|
|
266
|
+
RNC
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'parses interleave correctly' do
|
|
270
|
+
grammar = described_class.parse(input)
|
|
271
|
+
il = grammar.start.first.element.interleave
|
|
272
|
+
expect(il).not_to be_nil
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it 'round-trips interleave patterns' do
|
|
276
|
+
grammar1 = described_class.parse(input)
|
|
277
|
+
rnc = described_class.to_rnc(grammar1)
|
|
278
|
+
grammar2 = described_class.parse(rnc)
|
|
279
|
+
|
|
280
|
+
il = grammar2.start.first.element.interleave
|
|
281
|
+
expect(il).not_to be_nil
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
context 'with anyName patterns' do
|
|
286
|
+
let(:input) do
|
|
287
|
+
<<~RNC
|
|
288
|
+
start = element * {
|
|
289
|
+
text
|
|
290
|
+
}
|
|
291
|
+
RNC
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it 'parses anyName correctly' do
|
|
295
|
+
grammar = described_class.parse(input)
|
|
296
|
+
element = grammar.start.first.element
|
|
297
|
+
expect(element.anyName).not_to be_nil
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
context 'with nsName patterns' do
|
|
302
|
+
let(:input) do
|
|
303
|
+
<<~RNC
|
|
304
|
+
namespace ns = "http://example.org"
|
|
305
|
+
|
|
306
|
+
start = element ns:* {
|
|
307
|
+
text
|
|
308
|
+
}
|
|
309
|
+
RNC
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
it 'parses nsName correctly' do
|
|
313
|
+
grammar = described_class.parse(input)
|
|
314
|
+
expect(grammar.start.first.element).to be_a(Rng::Element)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
context 'with external references' do
|
|
319
|
+
let(:input) do
|
|
320
|
+
<<~RNC
|
|
321
|
+
start = element doc {
|
|
322
|
+
external "other.rnc"
|
|
323
|
+
}
|
|
324
|
+
RNC
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
it 'parses external references' do
|
|
328
|
+
expect { described_class.parse(input) }.not_to raise_error
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
context 'with parent references' do
|
|
333
|
+
let(:input) do
|
|
334
|
+
<<~RNC
|
|
335
|
+
grammar {
|
|
336
|
+
start = element outer {
|
|
337
|
+
grammar {
|
|
338
|
+
start = element inner {
|
|
339
|
+
parent ref
|
|
340
|
+
}
|
|
341
|
+
ref = element data { text }
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
ref = element sibling { text }
|
|
345
|
+
}
|
|
346
|
+
RNC
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
it 'parses parent references' do
|
|
350
|
+
expect { described_class.parse(input) }.not_to raise_error
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
context 'with list patterns' do
|
|
355
|
+
let(:input) do
|
|
356
|
+
<<~RNC
|
|
357
|
+
start = element values {
|
|
358
|
+
list {
|
|
359
|
+
xsd:int+
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
RNC
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
it 'parses list patterns' do
|
|
366
|
+
grammar = described_class.parse(input)
|
|
367
|
+
expect(grammar.start.first.element.list).not_to be_nil
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
context 'with notAllowed patterns' do
|
|
372
|
+
let(:input) do
|
|
373
|
+
<<~RNC
|
|
374
|
+
start = element doc {
|
|
375
|
+
notAllowed
|
|
376
|
+
}
|
|
377
|
+
RNC
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
it 'parses notAllowed correctly' do
|
|
381
|
+
grammar = described_class.parse(input)
|
|
382
|
+
expect(grammar.start.first.element.notAllowed).not_to be_nil
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
context 'with empty patterns' do
|
|
387
|
+
let(:input) do
|
|
388
|
+
<<~RNC
|
|
389
|
+
start = element doc {
|
|
390
|
+
empty
|
|
391
|
+
}
|
|
392
|
+
RNC
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
it 'parses empty correctly' do
|
|
396
|
+
grammar = described_class.parse(input)
|
|
397
|
+
expect(grammar.start.first.element.empty).not_to be_nil
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
context 'with data patterns with params' do
|
|
402
|
+
let(:input) do
|
|
403
|
+
<<~RNC
|
|
404
|
+
start = element value {
|
|
405
|
+
xsd:string { maxLength = "100" }
|
|
406
|
+
}
|
|
407
|
+
RNC
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
it 'parses data with parameters' do
|
|
411
|
+
grammar = described_class.parse(input)
|
|
412
|
+
data = grammar.start.first.element.data
|
|
413
|
+
expect(data).not_to be_nil
|
|
414
|
+
expect(data.param).not_to be_empty if data&.param
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
context 'with except patterns in anyName' do
|
|
419
|
+
let(:input) do
|
|
420
|
+
<<~RNC
|
|
421
|
+
start = element * - (reserved | special) {
|
|
422
|
+
text
|
|
423
|
+
}
|
|
424
|
+
RNC
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
it 'parses except in anyName' do
|
|
428
|
+
expect { described_class.parse(input) }.not_to raise_error
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Error Handling Tests
|
|
434
|
+
describe 'Error Handling' do
|
|
435
|
+
context 'with malformed RNC' do
|
|
436
|
+
it 'raises error for missing closing brace' do
|
|
437
|
+
input = 'element doc { text'
|
|
438
|
+
expect do
|
|
439
|
+
described_class.parse(input)
|
|
440
|
+
end.to raise_error(Parslet::ParseFailed)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
it 'raises error for invalid element syntax' do
|
|
444
|
+
input = 'element { text }'
|
|
445
|
+
expect do
|
|
446
|
+
described_class.parse(input)
|
|
447
|
+
end.to raise_error(Parslet::ParseFailed)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
it 'raises error for invalid occurrence marker' do
|
|
451
|
+
input = 'element doc { text ++ }'
|
|
452
|
+
expect do
|
|
453
|
+
described_class.parse(input)
|
|
454
|
+
end.to raise_error(Parslet::ParseFailed)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
it 'raises error for unclosed parentheses' do
|
|
458
|
+
input = 'element doc { (text }'
|
|
459
|
+
expect do
|
|
460
|
+
described_class.parse(input)
|
|
461
|
+
end.to raise_error(Parslet::ParseFailed)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
it 'raises error for invalid choice syntax' do
|
|
465
|
+
input = 'element doc { text | | text }'
|
|
466
|
+
expect do
|
|
467
|
+
described_class.parse(input)
|
|
468
|
+
end.to raise_error(Parslet::ParseFailed)
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
context 'with empty input' do
|
|
473
|
+
it 'returns empty grammar for empty string' do
|
|
474
|
+
result = described_class.parse('')
|
|
475
|
+
expect(result).to be_a(Rng::Grammar)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
it 'returns empty grammar for whitespace only' do
|
|
479
|
+
result = described_class.parse(" \n ")
|
|
480
|
+
expect(result).to be_a(Rng::Grammar)
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Performance Benchmarks
|
|
486
|
+
describe 'Performance' do
|
|
487
|
+
context 'with large schemas' do
|
|
488
|
+
# Find the largest schema file
|
|
489
|
+
rnc_files = Dir.glob('spec/fixtures/metanorma/*.rnc')
|
|
490
|
+
|
|
491
|
+
if rnc_files.any?
|
|
492
|
+
largest_file = rnc_files.max_by { |f| File.size(f) }
|
|
493
|
+
schema_name = File.basename(largest_file, '.rnc')
|
|
494
|
+
|
|
495
|
+
it "parses #{schema_name} in reasonable time" do
|
|
496
|
+
rnc = File.read(largest_file)
|
|
497
|
+
|
|
498
|
+
start_time = Time.now
|
|
499
|
+
10.times { described_class.parse(rnc) }
|
|
500
|
+
elapsed = Time.now - start_time
|
|
501
|
+
|
|
502
|
+
avg_time = elapsed / 10
|
|
503
|
+
|
|
504
|
+
# Should parse in under 2s per iteration
|
|
505
|
+
expect(avg_time).to be < 2.0
|
|
506
|
+
end
|
|
507
|
+
else
|
|
508
|
+
it 'has schema files for performance testing' do
|
|
509
|
+
skip 'No Metanorma schemas available for performance testing'
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
context 'with round-trip conversion' do
|
|
515
|
+
let(:input) do
|
|
516
|
+
<<~RNC
|
|
517
|
+
start = element addressBook {
|
|
518
|
+
element card {
|
|
519
|
+
attribute id { text },
|
|
520
|
+
element name { text },
|
|
521
|
+
element email { text }
|
|
522
|
+
}*
|
|
523
|
+
}
|
|
524
|
+
RNC
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
it 'performs round-trip conversion efficiently' do
|
|
528
|
+
start_time = Time.now
|
|
529
|
+
|
|
530
|
+
100.times do
|
|
531
|
+
grammar = described_class.parse(input)
|
|
532
|
+
rnc = described_class.to_rnc(grammar)
|
|
533
|
+
described_class.parse(rnc)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
elapsed = Time.now - start_time
|
|
537
|
+
avg_time = elapsed / 100
|
|
538
|
+
|
|
539
|
+
# Should complete in under 50ms per round-trip
|
|
540
|
+
expect(avg_time).to be < 0.05
|
|
65
541
|
end
|
|
66
542
|
end
|
|
67
543
|
end
|