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.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +63 -0
  3. data/.github/workflows/release.yml +8 -3
  4. data/.gitignore +11 -0
  5. data/.rubocop.yml +10 -7
  6. data/.rubocop_todo.yml +229 -23
  7. data/CHANGELOG.md +317 -0
  8. data/CLAUDE.md +139 -0
  9. data/Gemfile +11 -12
  10. data/README.adoc +1538 -11
  11. data/Rakefile +11 -3
  12. data/docs/Gemfile +8 -0
  13. data/docs/_config.yml +23 -0
  14. data/docs/getting-started/index.adoc +75 -0
  15. data/docs/guides/error-handling.adoc +137 -0
  16. data/docs/guides/external-references.adoc +128 -0
  17. data/docs/guides/index.adoc +24 -0
  18. data/docs/guides/parsing-rnc.adoc +141 -0
  19. data/docs/guides/parsing-rng-xml.adoc +81 -0
  20. data/docs/guides/rng-to-rnc.adoc +101 -0
  21. data/docs/guides/validation.adoc +85 -0
  22. data/docs/index.adoc +52 -0
  23. data/docs/reference/api.adoc +126 -0
  24. data/docs/reference/cli.adoc +182 -0
  25. data/docs/understanding/architecture.adoc +58 -0
  26. data/docs/understanding/rng-vs-rnc.adoc +118 -0
  27. data/exe/rng +5 -0
  28. data/lib/rng/any_name.rb +10 -8
  29. data/lib/rng/attribute.rb +28 -26
  30. data/lib/rng/choice.rb +24 -24
  31. data/lib/rng/cli.rb +607 -0
  32. data/lib/rng/data.rb +10 -10
  33. data/lib/rng/datatype_declaration.rb +26 -0
  34. data/lib/rng/define.rb +44 -41
  35. data/lib/rng/div.rb +36 -0
  36. data/lib/rng/documentation.rb +9 -0
  37. data/lib/rng/element.rb +39 -37
  38. data/lib/rng/empty.rb +7 -7
  39. data/lib/rng/except.rb +25 -25
  40. data/lib/rng/external_ref.rb +8 -8
  41. data/lib/rng/external_ref_resolver.rb +582 -0
  42. data/lib/rng/foreign_attribute.rb +26 -0
  43. data/lib/rng/foreign_element.rb +33 -0
  44. data/lib/rng/grammar.rb +14 -12
  45. data/lib/rng/group.rb +26 -24
  46. data/lib/rng/include.rb +5 -6
  47. data/lib/rng/include_processor.rb +461 -0
  48. data/lib/rng/interleave.rb +23 -23
  49. data/lib/rng/list.rb +22 -22
  50. data/lib/rng/mixed.rb +23 -23
  51. data/lib/rng/name.rb +7 -7
  52. data/lib/rng/namespace_declaration.rb +47 -0
  53. data/lib/rng/namespaces.rb +15 -0
  54. data/lib/rng/not_allowed.rb +7 -7
  55. data/lib/rng/ns_name.rb +9 -9
  56. data/lib/rng/one_or_more.rb +23 -23
  57. data/lib/rng/optional.rb +23 -23
  58. data/lib/rng/param.rb +8 -8
  59. data/lib/rng/parent_ref.rb +8 -8
  60. data/lib/rng/parse_tree_processor.rb +695 -0
  61. data/lib/rng/pattern.rb +7 -7
  62. data/lib/rng/ref.rb +8 -8
  63. data/lib/rng/rnc_builder.rb +927 -0
  64. data/lib/rng/rnc_parser.rb +605 -305
  65. data/lib/rng/rnc_to_rng_converter.rb +1408 -0
  66. data/lib/rng/schema_preamble.rb +73 -0
  67. data/lib/rng/schema_validator.rb +1622 -0
  68. data/lib/rng/start.rb +27 -25
  69. data/lib/rng/test_suite_parser.rb +168 -0
  70. data/lib/rng/text.rb +11 -8
  71. data/lib/rng/to_rnc.rb +4 -35
  72. data/lib/rng/value.rb +6 -7
  73. data/lib/rng/version.rb +1 -1
  74. data/lib/rng/zero_or_more.rb +23 -23
  75. data/lib/rng.rb +68 -17
  76. data/rng.gemspec +18 -19
  77. data/scripts/extract_spectest_resources.rb +96 -0
  78. data/spec/fixtures/compacttest.xml +2511 -0
  79. data/spec/fixtures/external/circular_a.rng +7 -0
  80. data/spec/fixtures/external/circular_b.rng +7 -0
  81. data/spec/fixtures/external/circular_main.rng +7 -0
  82. data/spec/fixtures/external/external_ref_lib.rng +7 -0
  83. data/spec/fixtures/external/external_ref_main.rng +7 -0
  84. data/spec/fixtures/external/include_lib.rng +7 -0
  85. data/spec/fixtures/external/include_main.rng +3 -0
  86. data/spec/fixtures/external/nested_chain.rng +6 -0
  87. data/spec/fixtures/external/nested_leaf.rng +7 -0
  88. data/spec/fixtures/external/nested_mid.rng +8 -0
  89. data/spec/fixtures/metanorma/3gpp.rnc +35 -0
  90. data/spec/fixtures/metanorma/3gpp.rng +105 -0
  91. data/spec/fixtures/metanorma/basicdoc.rnc +11 -0
  92. data/spec/fixtures/metanorma/bipm.rnc +148 -0
  93. data/spec/fixtures/metanorma/bipm.rng +376 -0
  94. data/spec/fixtures/metanorma/bsi.rnc +104 -0
  95. data/spec/fixtures/metanorma/bsi.rng +332 -0
  96. data/spec/fixtures/metanorma/csa.rnc +45 -0
  97. data/spec/fixtures/metanorma/csa.rng +131 -0
  98. data/spec/fixtures/metanorma/csd.rnc +43 -0
  99. data/spec/fixtures/metanorma/csd.rng +132 -0
  100. data/spec/fixtures/metanorma/gbstandard.rnc +99 -0
  101. data/spec/fixtures/metanorma/gbstandard.rng +316 -0
  102. data/spec/fixtures/metanorma/iec.rnc +49 -0
  103. data/spec/fixtures/metanorma/iec.rng +193 -0
  104. data/spec/fixtures/metanorma/ietf.rnc +275 -0
  105. data/spec/fixtures/metanorma/ietf.rng +925 -0
  106. data/spec/fixtures/metanorma/iho.rnc +58 -0
  107. data/spec/fixtures/metanorma/iho.rng +179 -0
  108. data/spec/fixtures/metanorma/isodoc.rnc +873 -0
  109. data/spec/fixtures/metanorma/isodoc.rng +2704 -0
  110. data/spec/fixtures/metanorma/isostandard-amd.rnc +43 -0
  111. data/spec/fixtures/metanorma/isostandard-amd.rng +108 -0
  112. data/spec/fixtures/metanorma/isostandard.rnc +166 -0
  113. data/spec/fixtures/metanorma/isostandard.rng +494 -0
  114. data/spec/fixtures/metanorma/itu.rnc +122 -0
  115. data/spec/fixtures/metanorma/itu.rng +377 -0
  116. data/spec/fixtures/metanorma/m3d.rnc +41 -0
  117. data/spec/fixtures/metanorma/m3d.rng +122 -0
  118. data/spec/fixtures/metanorma/mpfd.rnc +36 -0
  119. data/spec/fixtures/metanorma/mpfd.rng +95 -0
  120. data/spec/fixtures/metanorma/nist.rnc +77 -0
  121. data/spec/fixtures/metanorma/nist.rng +216 -0
  122. data/spec/fixtures/metanorma/ogc.rnc +51 -0
  123. data/spec/fixtures/metanorma/ogc.rng +151 -0
  124. data/spec/fixtures/metanorma/reqt.rnc +6 -0
  125. data/spec/fixtures/metanorma/rsd.rnc +36 -0
  126. data/spec/fixtures/metanorma/rsd.rng +95 -0
  127. data/spec/fixtures/metanorma/un.rnc +103 -0
  128. data/spec/fixtures/metanorma/un.rng +367 -0
  129. data/spec/fixtures/rnc/base.rnc +4 -0
  130. data/spec/fixtures/rnc/grammar_with_trailing.rnc +8 -0
  131. data/spec/fixtures/rnc/main_include_trailing.rnc +3 -0
  132. data/spec/fixtures/rnc/main_with_include.rnc +5 -0
  133. data/spec/fixtures/rnc/test_augment.rnc +10 -0
  134. data/spec/fixtures/rnc/test_isodoc_simple.rnc +9 -0
  135. data/spec/fixtures/rnc/top_level_include.rnc +8 -0
  136. data/spec/fixtures/spectest_external/case_10_4.7/x +3 -0
  137. data/spec/fixtures/spectest_external/case_10_4.7/y +7 -0
  138. data/spec/fixtures/spectest_external/case_11_4.7/x +3 -0
  139. data/spec/fixtures/spectest_external/case_12_4.7/x +3 -0
  140. data/spec/fixtures/spectest_external/case_13_4.7/x +3 -0
  141. data/spec/fixtures/spectest_external/case_13_4.7/y +3 -0
  142. data/spec/fixtures/spectest_external/case_14_4.7/x +7 -0
  143. data/spec/fixtures/spectest_external/case_15_4.7/x +7 -0
  144. data/spec/fixtures/spectest_external/case_16_4.7/x +5 -0
  145. data/spec/fixtures/spectest_external/case_17_4.7/x +5 -0
  146. data/spec/fixtures/spectest_external/case_18_4.7/x +7 -0
  147. data/spec/fixtures/spectest_external/case_19_4.7/level1.rng +9 -0
  148. data/spec/fixtures/spectest_external/case_19_4.7/level2.rng +7 -0
  149. data/spec/fixtures/spectest_external/case_1_4.5/sub1/x +3 -0
  150. data/spec/fixtures/spectest_external/case_1_4.5/sub3/x +3 -0
  151. data/spec/fixtures/spectest_external/case_1_4.5/x +3 -0
  152. data/spec/fixtures/spectest_external/case_20_4.6/x +3 -0
  153. data/spec/fixtures/spectest_external/case_2_4.5/x +3 -0
  154. data/spec/fixtures/spectest_external/case_3_4.6/x +3 -0
  155. data/spec/fixtures/spectest_external/case_4_4.6/x +3 -0
  156. data/spec/fixtures/spectest_external/case_5_4.6/x +1 -0
  157. data/spec/fixtures/spectest_external/case_6_4.6/x +5 -0
  158. data/spec/fixtures/spectest_external/case_7_4.6/x +1 -0
  159. data/spec/fixtures/spectest_external/case_7_4.6/y +1 -0
  160. data/spec/fixtures/spectest_external/case_8_4.7/x +7 -0
  161. data/spec/fixtures/spectest_external/case_9_4.7/x +7 -0
  162. data/spec/fixtures/spectest_external/resources.json +149 -0
  163. data/spec/rng/advanced_rnc_spec.rb +101 -0
  164. data/spec/rng/compacttest_spec.rb +197 -0
  165. data/spec/rng/datatype_declaration_spec.rb +28 -0
  166. data/spec/rng/div_spec.rb +207 -0
  167. data/spec/rng/external_ref_resolver_spec.rb +122 -0
  168. data/spec/rng/metanorma_conversion_spec.rb +159 -0
  169. data/spec/rng/namespace_declaration_spec.rb +60 -0
  170. data/spec/rng/namespace_support_spec.rb +199 -0
  171. data/spec/rng/rnc_parser_spec.rb +498 -22
  172. data/spec/rng/rnc_roundtrip_spec.rb +96 -82
  173. data/spec/rng/rng_generation_spec.rb +288 -0
  174. data/spec/rng/roundtrip_spec.rb +342 -0
  175. data/spec/rng/schema_preamble_spec.rb +145 -0
  176. data/spec/rng/schema_spec.rb +68 -64
  177. data/spec/rng/spectest_spec.rb +168 -90
  178. data/spec/rng_spec.rb +2 -2
  179. data/spec/spec_helper.rb +7 -42
  180. metadata +141 -8
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
3
+ require 'spec_helper'
4
4
 
5
5
  RSpec.describe Rng::RncParser do
6
- let(:parser) { described_class.new }
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 "correctly parses the schema" do
22
- result = parser.parse(input)
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.elements.first.name).to eq("addressBook")
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 "with attributes" do
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 "correctly parses attributes" do
41
- result = parser.parse(input)
42
- expect(result.start.elements.first.name).to eq("person")
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 "with nested elements" do
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 "correctly parses nested elements" do
61
- result = parser.parse(input)
62
- expect(result.start.elements.first.name).to eq("root")
63
- expect(result.start.elements.first.elements.map(&:name)).to eq(%w[child1 child2])
64
- expect(result.start.elements.first.elements.first.elements.first.name).to eq("grandchild")
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