lutaml-model 0.8.17 → 0.8.18

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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/downstream-performance.yml +27 -1
  3. data/.github/workflows/js-pr-check.yml +102 -0
  4. data/.github/workflows/js-sync.yml +51 -0
  5. data/.github/workflows/opal.yml +26 -6
  6. data/.github/workflows/performance.yml +32 -7
  7. data/.github/workflows/rake.yml +78 -32
  8. data/.gitmodules +6 -0
  9. data/.rubocop_todo.yml +8 -0
  10. data/Gemfile +16 -1
  11. data/README.adoc +267 -0
  12. data/Rakefile +166 -4
  13. data/docs/_guides/schema-import.adoc +42 -0
  14. data/docs/_guides/xml-mapping.adoc +21 -0
  15. data/docs/_pages/validation.adoc +65 -0
  16. data/lib/compat/opal/generate_boot.rb +123 -0
  17. data/lib/compat/opal/js_bundle_entry.rb +42 -0
  18. data/lib/compat/opal/lutaml_model_boot.rb +497 -0
  19. data/lib/compat/opal/moxml_boot.rb +68 -0
  20. data/lib/compat/opal/yaml_compat.rb +32 -0
  21. data/lib/lutaml/json.rb +1 -1
  22. data/lib/lutaml/key_value/transform.rb +3 -8
  23. data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -1
  24. data/lib/lutaml/key_value/transformation.rb +17 -5
  25. data/lib/lutaml/model/adapter_resolver.rb +2 -2
  26. data/lib/lutaml/model/attribute.rb +88 -11
  27. data/lib/lutaml/model/cached_type_resolver.rb +1 -1
  28. data/lib/lutaml/model/error/union_schema_unsupported_error.rb +11 -0
  29. data/lib/lutaml/model/global_context.rb +11 -8
  30. data/lib/lutaml/model/instrumentation.rb +2 -2
  31. data/lib/lutaml/model/mapping/mapping_rule.rb +0 -6
  32. data/lib/lutaml/model/register.rb +2 -2
  33. data/lib/lutaml/model/runtime_compatibility.rb +50 -0
  34. data/lib/lutaml/model/schema/class_loader.rb +58 -0
  35. data/lib/lutaml/model/schema/compiled_output.rb +83 -0
  36. data/lib/lutaml/model/schema/definitions/attribute.rb +32 -0
  37. data/lib/lutaml/model/schema/definitions/choice.rb +20 -0
  38. data/lib/lutaml/model/schema/definitions/facet.rb +40 -0
  39. data/lib/lutaml/model/schema/definitions/group_import.rb +22 -0
  40. data/lib/lutaml/model/schema/definitions/member_walk.rb +33 -0
  41. data/lib/lutaml/model/schema/definitions/model.rb +40 -0
  42. data/lib/lutaml/model/schema/definitions/namespace.rb +23 -0
  43. data/lib/lutaml/model/schema/definitions/restricted_type.rb +30 -0
  44. data/lib/lutaml/model/schema/definitions/sequence.rb +19 -0
  45. data/lib/lutaml/model/schema/definitions/simple_content.rb +22 -0
  46. data/lib/lutaml/model/schema/definitions/transform_facet.rb +21 -0
  47. data/lib/lutaml/model/schema/definitions/type_ref.rb +20 -0
  48. data/lib/lutaml/model/schema/definitions/union_type.rb +28 -0
  49. data/lib/lutaml/model/schema/definitions/xml_root.rb +20 -0
  50. data/lib/lutaml/model/schema/definitions.rb +24 -0
  51. data/lib/lutaml/model/schema/file_writer.rb +70 -0
  52. data/lib/lutaml/model/schema/generator/definitions_collection.rb +16 -2
  53. data/lib/lutaml/model/schema/generator/property.rb +21 -4
  54. data/lib/lutaml/model/schema/module_nesting.rb +31 -0
  55. data/lib/lutaml/model/schema/namespace_naming.rb +46 -0
  56. data/lib/lutaml/model/schema/registry_generator.rb +115 -0
  57. data/lib/lutaml/model/schema/renderers/base.rb +40 -0
  58. data/lib/lutaml/model/schema/renderers/mappings.rb +72 -0
  59. data/lib/lutaml/model/schema/renderers/member_decls.rb +115 -0
  60. data/lib/lutaml/model/schema/renderers/model.rb +121 -0
  61. data/lib/lutaml/model/schema/renderers/namespace.rb +30 -0
  62. data/lib/lutaml/model/schema/renderers/registration.rb +64 -0
  63. data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +93 -0
  64. data/lib/lutaml/model/schema/renderers/restricted_type.rb +88 -0
  65. data/lib/lutaml/model/schema/renderers/union.rb +83 -0
  66. data/lib/lutaml/model/schema/renderers.rb +19 -0
  67. data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +47 -0
  68. data/lib/lutaml/model/schema/rnc_compiler.rb +62 -0
  69. data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +91 -0
  70. data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +392 -0
  71. data/lib/lutaml/model/schema/rng_compiler/member_collector.rb +30 -0
  72. data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +152 -0
  73. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +125 -0
  74. data/lib/lutaml/model/schema/rng_compiler.rb +220 -0
  75. data/lib/lutaml/model/schema/templates.rb +162 -0
  76. data/lib/lutaml/model/schema/xml_compiler/element_order.rb +41 -0
  77. data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +31 -107
  78. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +191 -0
  79. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +216 -0
  80. data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +153 -0
  81. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +167 -0
  82. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +87 -0
  83. data/lib/lutaml/model/schema/xml_compiler.rb +38 -517
  84. data/lib/lutaml/model/schema.rb +19 -0
  85. data/lib/lutaml/model/serialize/attribute_definition.rb +14 -2
  86. data/lib/lutaml/model/serialize/builder.rb +15 -2
  87. data/lib/lutaml/model/serialize/value_mapping.rb +9 -57
  88. data/lib/lutaml/model/serialize.rb +6 -9
  89. data/lib/lutaml/model/services/rule_value_extractor.rb +2 -3
  90. data/lib/lutaml/model/services.rb +0 -2
  91. data/lib/lutaml/model/store.rb +2 -1
  92. data/lib/lutaml/model/toml.rb +1 -1
  93. data/lib/lutaml/model/transform.rb +17 -39
  94. data/lib/lutaml/model/transformation_registry.rb +1 -1
  95. data/lib/lutaml/model/type.rb +1 -0
  96. data/lib/lutaml/model/union.rb +320 -0
  97. data/lib/lutaml/model/utils.rb +6 -0
  98. data/lib/lutaml/model/validation.rb +1 -4
  99. data/lib/lutaml/model/version.rb +1 -1
  100. data/lib/lutaml/model.rb +18 -0
  101. data/lib/lutaml/toml.rb +1 -1
  102. data/lib/lutaml/xml/adapter.rb +1 -1
  103. data/lib/lutaml/xml/adapter_loader.rb +1 -1
  104. data/lib/lutaml/xml/builder/base.rb +1 -1
  105. data/lib/lutaml/xml/builder.rb +1 -1
  106. data/lib/lutaml/xml/error/schema_validation_error.rb +25 -0
  107. data/lib/lutaml/xml/model_transform.rb +5 -1
  108. data/lib/lutaml/xml/schema/relaxng_schema.rb +5 -0
  109. data/lib/lutaml/xml/schema/xsd_schema.rb +4 -0
  110. data/lib/lutaml/xml/serialization/format_conversion.rb +48 -0
  111. data/lib/lutaml/xml/serialization/instance_methods.rb +13 -0
  112. data/lib/lutaml/xml/transformation/element_builder.rb +21 -8
  113. data/lib/lutaml/xml/transformation/ordered_applier.rb +6 -0
  114. data/lib/lutaml/xml/xsd_validator.rb +66 -0
  115. data/lib/lutaml/xml.rb +53 -12
  116. data/lutaml-model.gemspec +4 -2
  117. data/spec/fixtures/xml/schema/rnc/address_book.rnc +15 -0
  118. data/spec/fixtures/xml/schema/rnc/book_features.rnc +18 -0
  119. data/spec/fixtures/xml/schema/rnc/includes/book.rnc +6 -0
  120. data/spec/fixtures/xml/schema/rnc/includes/circular_a.rnc +8 -0
  121. data/spec/fixtures/xml/schema/rnc/includes/circular_b.rnc +8 -0
  122. data/spec/fixtures/xml/schema/rnc/includes/library.rnc +8 -0
  123. data/spec/fixtures/xml/schema/rng/address_book.rng +16 -0
  124. data/spec/fixtures/xml/schema/rng/book.rng +27 -0
  125. data/spec/fixtures/xml/schema/rng/fixed_value.rng +11 -0
  126. data/spec/fixtures/xml/schema/rng/fragment_in_choice.rng +30 -0
  127. data/spec/fixtures/xml/schema/rng/full_name.rng +39 -0
  128. data/spec/fixtures/xml/schema/rng/integer_range.rng +32 -0
  129. data/spec/fixtures/xml/schema/rng/list_type.rng +12 -0
  130. data/spec/fixtures/xml/schema/rng/namespaced.rng +9 -0
  131. data/spec/fixtures/xml/schema/rng/paragraph.rng +13 -0
  132. data/spec/fixtures/xml/schema/rng/person.rng +23 -0
  133. data/spec/fixtures/xml/schema/rng/union.rng +14 -0
  134. data/spec/fixtures/xml/validate_xml_with_address.xsd +13 -0
  135. data/spec/fixtures/xml/validate_xml_with_contact.xsd +11 -0
  136. data/spec/fixtures/xml/validate_xml_with_person.xsd +11 -0
  137. data/spec/fixtures/xml/validate_xml_with_person_strict.xsd +17 -0
  138. data/spec/fixtures/xml/validate_xml_with_product.xsd +37 -0
  139. data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
  140. data/spec/lutaml/model/attribute_spec.rb +169 -12
  141. data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
  142. data/spec/lutaml/model/collection_spec.rb +18 -0
  143. data/spec/lutaml/model/mixed_content_spec.rb +171 -0
  144. data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
  145. data/spec/lutaml/model/ordered_content_spec.rb +30 -0
  146. data/spec/lutaml/model/raw_element_spec.rb +125 -0
  147. data/spec/lutaml/model/rule_value_extractor_spec.rb +7 -14
  148. data/spec/lutaml/model/runtime_compatibility_spec.rb +25 -0
  149. data/spec/lutaml/model/schema/renderers/model_spec.rb +149 -0
  150. data/spec/lutaml/model/schema/renderers/namespace_spec.rb +36 -0
  151. data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +75 -0
  152. data/spec/lutaml/model/schema/renderers/union_spec.rb +58 -0
  153. data/spec/lutaml/model/transform_apply_value_map_spec.rb +64 -0
  154. data/spec/lutaml/model/union_attribute_spec.rb +658 -0
  155. data/spec/lutaml/model/union_spec.rb +257 -0
  156. data/spec/lutaml/model/utils_spec.rb +14 -0
  157. data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
  158. data/spec/lutaml/xml/schema/compiler_spec.rb +21 -4
  159. data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +404 -0
  160. data/spec/lutaml/xml/schema/rng_compiler_spec.rb +893 -0
  161. data/spec/lutaml/xml/validate_xml_with_constructs_spec.rb +222 -0
  162. data/spec/lutaml/xml/validate_xml_with_spec.rb +186 -0
  163. data/spec/spec_helper.rb +28 -11
  164. data/spec/support/opal.rb +5 -2
  165. metadata +97 -31
  166. data/lib/lutaml/model/schema/xml_compiler/attribute.rb +0 -106
  167. data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +0 -47
  168. data/lib/lutaml/model/schema/xml_compiler/choice.rb +0 -67
  169. data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +0 -27
  170. data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +0 -34
  171. data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +0 -173
  172. data/lib/lutaml/model/schema/xml_compiler/element.rb +0 -121
  173. data/lib/lutaml/model/schema/xml_compiler/group.rb +0 -113
  174. data/lib/lutaml/model/schema/xml_compiler/restriction.rb +0 -104
  175. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -52
  176. data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +0 -40
  177. data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +0 -265
  178. data/lib/lutaml/model/schema/xml_compiler/xml_namespace_class.rb +0 -110
  179. data/lib/lutaml/model/services/default_value_resolver.rb +0 -60
  180. data/spec/lutaml/model/services/default_value_resolver_spec.rb +0 -162
  181. data/spec/lutaml/xml/schema/compiler/attribute_group_spec.rb +0 -71
  182. data/spec/lutaml/xml/schema/compiler/attribute_spec.rb +0 -67
  183. data/spec/lutaml/xml/schema/compiler/choice_spec.rb +0 -74
  184. data/spec/lutaml/xml/schema/compiler/complex_content_restriction_spec.rb +0 -60
  185. data/spec/lutaml/xml/schema/compiler/complex_content_spec.rb +0 -41
  186. data/spec/lutaml/xml/schema/compiler/complex_type_spec.rb +0 -202
  187. data/spec/lutaml/xml/schema/compiler/element_spec.rb +0 -68
  188. data/spec/lutaml/xml/schema/compiler/group_spec.rb +0 -90
  189. data/spec/lutaml/xml/schema/compiler/restriction_spec.rb +0 -77
  190. data/spec/lutaml/xml/schema/compiler/sequence_spec.rb +0 -65
  191. data/spec/lutaml/xml/schema/compiler/simple_content_spec.rb +0 -61
  192. data/spec/lutaml/xml/schema/compiler/simple_type_spec.rb +0 -184
@@ -0,0 +1,893 @@
1
+ require "rng"
2
+ require "spec_helper"
3
+ require "lutaml/model/schema"
4
+ require "tmpdir"
5
+
6
+ RSpec.describe Lutaml::Model::Schema::RngCompiler do
7
+ # Reset :default context before each test so generated classes from one
8
+ # context don't leak into another. Matches XSD compiler spec setup.
9
+ before do
10
+ default_ctx = Lutaml::Model::GlobalContext.context(:default)
11
+ if default_ctx
12
+ default_ctx.registry.clear
13
+ Lutaml::Model::Type.register_builtin_types_in(default_ctx.registry)
14
+ end
15
+ end
16
+
17
+ describe ".to_models" do
18
+ context "with the address_book RNG schema (start element + zeroOrMore + ref + text + fragment define)" do
19
+ before do
20
+ stub_const("RngAddressBookSpec", Module.new)
21
+ described_class.to_models(
22
+ schema,
23
+ output_dir: dir,
24
+ create_files: true,
25
+ module_namespace: "RngAddressBookSpec",
26
+ )
27
+ require File.join(dir, "rngaddressbookspec_registry.rb")
28
+ RngAddressBookSpec.register_all
29
+ end
30
+
31
+ after do
32
+ FileUtils.rm_rf(dir)
33
+ end
34
+
35
+ let(:dir) { Dir.mktmpdir }
36
+ let(:schema) { File.read("spec/fixtures/xml/schema/rng/address_book.rng") }
37
+
38
+ let(:valid_xml) do
39
+ <<~XML
40
+ <addressBook>
41
+ <card>
42
+ <name>Alice</name>
43
+ <email>alice@example.com</email>
44
+ </card>
45
+ <card>
46
+ <name>Bob</name>
47
+ <email>bob@example.com</email>
48
+ </card>
49
+ </addressBook>
50
+ XML
51
+ end
52
+
53
+ it "writes per-class files and a central registry" do
54
+ module_dir = File.join(dir, "rngaddressbookspec")
55
+ expect(File).to exist(File.join(dir, "rngaddressbookspec_registry.rb"))
56
+ expect(File).to exist(File.join(module_dir, "address_book.rb"))
57
+ expect(File).to exist(File.join(module_dir, "card.rb"))
58
+ expect(File).to exist(File.join(module_dir, "card_content.rb"))
59
+ end
60
+
61
+ it "loads the AddressBook constant after register_all" do
62
+ expect(defined?(RngAddressBookSpec::AddressBook)).to eq("constant")
63
+ expect(defined?(RngAddressBookSpec::Card)).to eq("constant")
64
+ expect(defined?(RngAddressBookSpec::CardContent)).to eq("constant")
65
+ end
66
+
67
+ it "round-trips a valid XML document through the generated classes" do
68
+ book = RngAddressBookSpec::AddressBook.from_xml(valid_xml)
69
+ expect(book.card.size).to eq(2)
70
+ expect(book.card[0].name).to eq("Alice")
71
+ expect(book.card[1].email).to eq("bob@example.com")
72
+ expect(book.to_xml).to be_xml_equivalent_to(valid_xml)
73
+ end
74
+ end
75
+
76
+ context "with the book RNG schema (attribute + optional + oneOrMore + group + choice + empty + data)" do
77
+ before do
78
+ stub_const("RngBookSpec", Module.new)
79
+ described_class.to_models(
80
+ schema,
81
+ output_dir: dir,
82
+ create_files: true,
83
+ module_namespace: "RngBookSpec",
84
+ )
85
+ require File.join(dir, "rngbookspec_registry.rb")
86
+ RngBookSpec.register_all
87
+ end
88
+
89
+ after do
90
+ FileUtils.rm_rf(dir)
91
+ end
92
+
93
+ let(:dir) { Dir.mktmpdir }
94
+ let(:schema) { File.read("spec/fixtures/xml/schema/rng/book.rng") }
95
+
96
+ let(:hardcover_xml) do
97
+ <<~XML
98
+ <book isbn="978-0-13-110362-7">
99
+ <title>The C Programming Language</title>
100
+ <author>Brian Kernighan</author>
101
+ <author>Dennis Ritchie</author>
102
+ <publisher>Prentice Hall</publisher>
103
+ <year>1988</year>
104
+ <hardcover/>
105
+ </book>
106
+ XML
107
+ end
108
+
109
+ let(:paperback_xml) do
110
+ <<~XML
111
+ <book isbn="978-0-201-83595-3">
112
+ <title>The Mythical Man-Month</title>
113
+ <subtitle>Essays on Software Engineering</subtitle>
114
+ <author>Frederick P. Brooks Jr.</author>
115
+ <publisher>Addison-Wesley</publisher>
116
+ <year>1995</year>
117
+ <paperback/>
118
+ </book>
119
+ XML
120
+ end
121
+
122
+ it "compiles <attribute> as :attribute kind with map_attribute" do
123
+ book = RngBookSpec::Book.from_xml(hardcover_xml)
124
+ expect(book.isbn).to eq("978-0-13-110362-7")
125
+ end
126
+
127
+ it "handles <oneOrMore> as a collection attribute" do
128
+ book = RngBookSpec::Book.from_xml(hardcover_xml)
129
+ expect(book.author).to eq(["Brian Kernighan", "Dennis Ritchie"])
130
+ end
131
+
132
+ it "leaves <optional> elements absent when not in the XML" do
133
+ book = RngBookSpec::Book.from_xml(hardcover_xml)
134
+ expect(book.subtitle).to be_nil
135
+ end
136
+
137
+ it "reads <optional> elements when present" do
138
+ book = RngBookSpec::Book.from_xml(paperback_xml)
139
+ expect(book.subtitle).to eq("Essays on Software Engineering")
140
+ end
141
+
142
+ it "compiles <data type=\"integer\"/> as :integer (slice 3)" do
143
+ book = RngBookSpec::Book.from_xml(hardcover_xml)
144
+ expect(book.year).to eq(1988)
145
+ end
146
+
147
+ it "compiles <choice> alternatives, with empty-bodied elements as classes" do
148
+ book = RngBookSpec::Book.from_xml(hardcover_xml)
149
+ expect(book.hardcover).to be_a(RngBookSpec::Hardcover)
150
+ expect(book.paperback).to be_nil
151
+ end
152
+ end
153
+
154
+ context "with the person RNG schema (start ref + multi-define + cross-references)" do
155
+ before do
156
+ stub_const("RngPersonSpec", Module.new)
157
+ described_class.to_models(
158
+ schema,
159
+ output_dir: dir,
160
+ create_files: true,
161
+ module_namespace: "RngPersonSpec",
162
+ )
163
+ require File.join(dir, "rngpersonspec_registry.rb")
164
+ RngPersonSpec.register_all
165
+ end
166
+
167
+ after do
168
+ FileUtils.rm_rf(dir)
169
+ end
170
+
171
+ let(:dir) { Dir.mktmpdir }
172
+ let(:schema) { File.read("spec/fixtures/xml/schema/rng/person.rng") }
173
+
174
+ let(:xml) do
175
+ <<~XML
176
+ <Person>
177
+ <name>Alice</name>
178
+ <age>30</age>
179
+ <Address>
180
+ <street>1 Main St</street>
181
+ <city>Springfield</city>
182
+ </Address>
183
+ </Person>
184
+ XML
185
+ end
186
+
187
+ it "generates one class per define" do
188
+ expect(defined?(RngPersonSpec::Person)).to eq("constant")
189
+ expect(defined?(RngPersonSpec::Address)).to eq("constant")
190
+ end
191
+
192
+ it "resolves <ref> as a typed attribute pointing to the referenced class" do
193
+ person = RngPersonSpec::Person.from_xml(xml)
194
+ expect(person.address).to be_a(RngPersonSpec::Address)
195
+ expect(person.address.street).to eq("1 Main St")
196
+ expect(person.address.city).to eq("Springfield")
197
+ end
198
+
199
+ it "round-trips XML through the cross-referenced classes" do
200
+ person = RngPersonSpec::Person.from_xml(xml)
201
+ expect(person.to_xml).to be_xml_equivalent_to(xml)
202
+ end
203
+ end
204
+
205
+ context "with the integer_range RNG schema (restrictions + enumerations as SimpleType subclasses)" do
206
+ before do
207
+ stub_const("RngRestrictionSpec", Module.new)
208
+ described_class.to_models(
209
+ schema,
210
+ output_dir: dir,
211
+ create_files: true,
212
+ module_namespace: "RngRestrictionSpec",
213
+ )
214
+ require File.join(dir, "rngrestrictionspec_registry.rb")
215
+ RngRestrictionSpec.register_all
216
+ end
217
+
218
+ after do
219
+ FileUtils.rm_rf(dir)
220
+ end
221
+
222
+ let(:dir) { Dir.mktmpdir }
223
+ let(:schema) { File.read("spec/fixtures/xml/schema/rng/integer_range.rng") }
224
+
225
+ it "generates a SimpleType subclass for <data> with restrictions" do
226
+ src = File.read(File.join(dir, "rngrestrictionspec", "st_integer_range.rb"))
227
+ expect(src).to include("class StIntegerRange < Lutaml::Model::Type::Integer")
228
+ expect(src).to include("options[:min] = 1")
229
+ expect(src).to include("options[:max] = 255")
230
+ end
231
+
232
+ it "generates a SimpleType subclass for <choice> of <value> enumerations" do
233
+ src = File.read(File.join(dir, "rngrestrictionspec", "st_color.rb"))
234
+ expect(src).to include("class StColor < Lutaml::Model::Type::String")
235
+ expect(src).to include('options[:values] = [super("red"), super("green"), super("blue")]')
236
+ end
237
+
238
+ it "references SimpleTypes by registered symbol from the consuming class" do
239
+ src = File.read(File.join(dir, "rngrestrictionspec", "thing.rb"))
240
+ expect(src).to include("attribute :val, :st_integer_range")
241
+ expect(src).to include("attribute :color, :st_color")
242
+ end
243
+
244
+ it "accepts XML that satisfies the restrictions" do
245
+ thing = RngRestrictionSpec::Thing.from_xml(
246
+ '<thing val="42"><color>red</color></thing>',
247
+ )
248
+ expect(thing.val).to eq(42)
249
+ expect(thing.color).to eq("red")
250
+ end
251
+
252
+ it "raises MaxBoundError for out-of-range integers" do
253
+ expect do
254
+ RngRestrictionSpec::Thing.from_xml(
255
+ '<thing val="500"><color>red</color></thing>',
256
+ )
257
+ end.to raise_error(Lutaml::Model::Type::MaxBoundError)
258
+ end
259
+
260
+ it "raises InvalidValueError for non-enum values" do
261
+ expect do
262
+ RngRestrictionSpec::Thing.from_xml(
263
+ '<thing val="42"><color>purple</color></thing>',
264
+ )
265
+ end.to raise_error(Lutaml::Model::Type::InvalidValueError)
266
+ end
267
+ end
268
+
269
+ context "with inline restrictions (anonymous SimpleType emission)" do
270
+ let(:rng) do
271
+ <<~RNG
272
+ <grammar xmlns="http://relaxng.org/ns/structure/1.0">
273
+ <start>
274
+ <element name="game">
275
+ <attribute name="age">
276
+ <data type="integer">
277
+ <param name="minInclusive">3</param>
278
+ <param name="maxInclusive">18</param>
279
+ </data>
280
+ </attribute>
281
+ <element name="rating">
282
+ <choice>
283
+ <value>E</value>
284
+ <value>T</value>
285
+ <value>M</value>
286
+ </choice>
287
+ </element>
288
+ </element>
289
+ </start>
290
+ </grammar>
291
+ RNG
292
+ end
293
+
294
+ let(:sources) { described_class.to_models(rng) }
295
+
296
+ it "generates anonymous SimpleTypes named after the host element/attribute" do
297
+ expect(sources.keys).to contain_exactly("Game", "AgeType", "RatingType")
298
+ end
299
+
300
+ it "references inline restrictions by registered type symbol" do
301
+ expect(sources["Game"]).to include("attribute :age, :age_type")
302
+ expect(sources["Game"]).to include("attribute :rating, :rating_type")
303
+ end
304
+
305
+ it "emits the inline integer restriction as a Type::Integer subclass" do
306
+ expect(sources["AgeType"]).to include("class AgeType < Lutaml::Model::Type::Integer")
307
+ expect(sources["AgeType"]).to include("options[:min] = 3")
308
+ expect(sources["AgeType"]).to include("options[:max] = 18")
309
+ end
310
+
311
+ it "emits the inline enumeration as a Type::String subclass with values" do
312
+ expect(sources["RatingType"]).to include("class RatingType < Lutaml::Model::Type::String")
313
+ expect(sources["RatingType"]).to include('options[:values] = [super("E"), super("T"), super("M")]')
314
+ end
315
+ end
316
+
317
+ context "with the FullNameType regression fixture from issue #9" do
318
+ let(:sources) do
319
+ described_class.to_models(
320
+ File.read("spec/fixtures/xml/schema/rng/full_name.rng"),
321
+ )
322
+ end
323
+
324
+ it "preserves document order in the XML mappings" do
325
+ expected_order = %w[
326
+ abbrev prefix forename initials surname addition
327
+ completeName biblionote variantname
328
+ ]
329
+ mapping_lines = sources["FullNameType"].lines.grep(/map_element "/)
330
+ actual_order = mapping_lines.map { |l| l[/map_element "(\w+)"/, 1] }
331
+ expect(actual_order).to eq(expected_order)
332
+ end
333
+
334
+ it "wraps grouped mappings in `sequence do ... end` inside the xml block" do
335
+ # The <group> inside <choice> becomes a sequence wrapper in the XML
336
+ # mapping section (matching XSD compiler's behavior).
337
+ xml_block = sources["FullNameType"][/xml do.*?^ end/m]
338
+ expect(xml_block).to include("sequence do")
339
+ seq_block = xml_block[/sequence do.*?end/m]
340
+ order = seq_block.scan(/map_element "(\w+)"/).flatten
341
+ expect(order).to eq(%w[prefix forename initials surname addition])
342
+ end
343
+
344
+ it "emits choice alternatives in document order" do
345
+ # The <choice> contains a <group>(prefix, forename, initials, surname,
346
+ # addition) followed by completeName. After flattening (matching the
347
+ # XSD compiler's behavior), all six appear in document order inside
348
+ # the choice block.
349
+ choice_block = sources["FullNameType"][/choice do.*?end/m]
350
+ expect(choice_block).not_to be_nil
351
+ attrs = choice_block.scan(/attribute :(\w+)/).flatten
352
+ expect(attrs).to eq(
353
+ %w[prefix forename formatted_initials surname addition complete_name],
354
+ )
355
+ end
356
+ end
357
+
358
+ context "with a namespaced grammar (slice 4i)" do
359
+ let(:rng) do
360
+ <<~RNG
361
+ <grammar xmlns="http://relaxng.org/ns/structure/1.0" ns="http://example.com/books">
362
+ <start>
363
+ <element name="book">
364
+ <element name="title"><text/></element>
365
+ </element>
366
+ </start>
367
+ </grammar>
368
+ RNG
369
+ end
370
+
371
+ let(:sources) { described_class.to_models(rng) }
372
+
373
+ it "generates a XmlNamespace subclass for the grammar ns" do
374
+ ns_name = sources.keys.find { |k| k.include?("Namespace") }
375
+ expect(ns_name).not_to be_nil
376
+ expect(sources[ns_name]).to include("< Lutaml::Xml::W3c::XmlNamespace")
377
+ expect(sources[ns_name]).to include('uri "http://example.com/books"')
378
+ end
379
+
380
+ it "references the namespace class from each generated class" do
381
+ expect(sources["Book"]).to match(/namespace \w+Namespace/)
382
+ end
383
+
384
+ it "adds a require_relative for the namespace class" do
385
+ expect(sources["Book"]).to match(/require_relative ".*namespace"/)
386
+ end
387
+ end
388
+
389
+ context "with an element namespaced via prefix (no grammar default ns)" do
390
+ let(:sources) do
391
+ described_class.to_models(<<~RNG)
392
+ <grammar xmlns="http://relaxng.org/ns/structure/1.0">
393
+ <start>
394
+ <element name="root" ns="urn:test">
395
+ <element name="child" ns="urn:test"><text/></element>
396
+ </element>
397
+ </start>
398
+ </grammar>
399
+ RNG
400
+ end
401
+
402
+ it "assigns a class-level namespace to the namespaced element model" do
403
+ expect(sources["Root"]).to match(/namespace \w+Namespace/)
404
+ end
405
+
406
+ it "generates a XmlNamespace subclass carrying the element ns uri" do
407
+ ns_name = sources.keys.find { |k| k.include?("Namespace") }
408
+ expect(ns_name).not_to be_nil
409
+ expect(sources[ns_name]).to include('uri "urn:test"')
410
+ end
411
+ end
412
+
413
+ context "with a child declaring ns='' (a no-namespace override)" do
414
+ let(:sources) do
415
+ described_class.to_models(<<~RNG)
416
+ <grammar xmlns="http://relaxng.org/ns/structure/1.0" ns="urn:d">
417
+ <start>
418
+ <element name="root">
419
+ <element name="child" ns=""><text/></element>
420
+ </element>
421
+ </start>
422
+ </grammar>
423
+ RNG
424
+ end
425
+
426
+ # lutaml expresses namespaces only at class level, so a per-element
427
+ # ns='' override cannot be represented; the guard's job is only to keep
428
+ # ns='' from registering a bogus empty-URI namespace that would clobber
429
+ # the grammar default. It emits exactly the one grammar-default namespace.
430
+ it "keeps the grammar default namespace uri intact" do
431
+ ns_names = sources.keys.select { |k| k.include?("Namespace") }
432
+ expect(ns_names.size).to eq(1)
433
+ expect(sources[ns_names.first]).to include('uri "urn:d"')
434
+ expect(sources[ns_names.first]).not_to include('uri ""')
435
+ end
436
+ end
437
+
438
+ context "with documentation annotations (slice 4h)" do
439
+ let(:rng) do
440
+ <<~RNG
441
+ <grammar xmlns="http://relaxng.org/ns/structure/1.0"
442
+ xmlns:a="http://relaxng.org/ns/compatibility/annotations/1.0">
443
+ <start>
444
+ <element name="book">
445
+ <a:documentation>A book record with title and author.</a:documentation>
446
+ <element name="title">
447
+ <a:documentation>The book title.</a:documentation>
448
+ <text/>
449
+ </element>
450
+ <element name="author">
451
+ <a:documentation>The book author.</a:documentation>
452
+ <text/>
453
+ </element>
454
+ </element>
455
+ </start>
456
+ </grammar>
457
+ RNG
458
+ end
459
+
460
+ let(:sources) { described_class.to_models(rng) }
461
+
462
+ it "emits class-level documentation as a Ruby comment above the class" do
463
+ expect(sources["Book"]).to include("# A book record with title and author.\nclass Book")
464
+ end
465
+
466
+ it "emits attribute-level documentation as a Ruby comment above the attribute" do
467
+ expect(sources["Book"]).to include("# The book title.\n attribute :title")
468
+ expect(sources["Book"]).to include("# The book author.\n attribute :author")
469
+ end
470
+ end
471
+
472
+ context "with the paragraph RNG schema (<mixed>)" do
473
+ before do
474
+ stub_const("RngParaSpec", Module.new)
475
+ described_class.to_models(
476
+ schema,
477
+ output_dir: dir,
478
+ create_files: true,
479
+ module_namespace: "RngParaSpec",
480
+ )
481
+ require File.join(dir, "rngparaspec_registry.rb")
482
+ RngParaSpec.register_all
483
+ end
484
+
485
+ after do
486
+ FileUtils.rm_rf(dir)
487
+ end
488
+
489
+ let(:dir) { Dir.mktmpdir }
490
+ let(:schema) { File.read("spec/fixtures/xml/schema/rng/paragraph.rng") }
491
+
492
+ it "emits mixed_content for the <mixed> wrapper" do
493
+ para_source = File.read(File.join(dir, "rngparaspec", "para.rb"))
494
+ expect(para_source).to include("mixed_content")
495
+ end
496
+ end
497
+
498
+ context "when classes are generated but files are not created" do
499
+ let(:classes_hash) do
500
+ described_class.to_models(
501
+ File.read("spec/fixtures/xml/schema/rng/address_book.rng"),
502
+ )
503
+ end
504
+
505
+ it "returns a Hash of class_name => Ruby source" do
506
+ expect(classes_hash).to be_a(Hash)
507
+ expect(classes_hash.keys).to contain_exactly(
508
+ "AddressBook", "Card", "CardContent"
509
+ )
510
+ classes_hash.each_value { |src| expect(src).to start_with("# frozen_string_literal: true") }
511
+ end
512
+ end
513
+
514
+ context "round-trip with Lutaml::Xml::Schema::RelaxngSchema.generate" do
515
+ before do
516
+ stub_const("RngRtAddress", Class.new(Lutaml::Model::Serializable) do
517
+ attribute :street, :string
518
+ attribute :city, :string
519
+
520
+ xml do
521
+ element "RngRtAddress"
522
+ map_element "street", to: :street
523
+ map_element "city", to: :city
524
+ end
525
+ end)
526
+
527
+ stub_const("RngRtPerson", Class.new(Lutaml::Model::Serializable) do
528
+ attribute :name, :string
529
+ attribute :age, :integer
530
+ attribute :address, RngRtAddress
531
+
532
+ xml do
533
+ element "RngRtPerson"
534
+ map_element "name", to: :name
535
+ map_element "age", to: :age
536
+ map_element "RngRtAddress", to: :address
537
+ end
538
+ end)
539
+ end
540
+
541
+ it "round-trips primitive-typed attributes through generate + compile" do
542
+ rng = Lutaml::Xml::Schema::RelaxngSchema.generate(RngRtPerson)
543
+ sources = described_class.to_models(rng)
544
+
545
+ expect(sources["RngRtPerson"]).to include("attribute :name, :string")
546
+ expect(sources["RngRtPerson"]).to include("attribute :age, :integer")
547
+ end
548
+
549
+ it "round-trips typed reference attributes" do
550
+ rng = Lutaml::Xml::Schema::RelaxngSchema.generate(RngRtPerson)
551
+ sources = described_class.to_models(rng)
552
+
553
+ expect(sources["RngRtPerson"]).to include(
554
+ "attribute :rng_rt_address, RngRtAddress",
555
+ )
556
+ end
557
+ end
558
+
559
+ context "e2e: namespaced grammar loads + parses namespaced XML" do
560
+ before do
561
+ stub_const("RngNamespacedSpec", Module.new)
562
+ described_class.to_models(
563
+ File.read("spec/fixtures/xml/schema/rng/namespaced.rng"),
564
+ output_dir: dir,
565
+ create_files: true,
566
+ module_namespace: "RngNamespacedSpec",
567
+ )
568
+ require File.join(dir, "rngnamespacedspec_registry.rb")
569
+ RngNamespacedSpec.register_all
570
+ end
571
+
572
+ after do
573
+ FileUtils.rm_rf(dir)
574
+ end
575
+
576
+ let(:dir) { Dir.mktmpdir }
577
+ let(:xml) do
578
+ <<~XML
579
+ <book xmlns="http://example.com/books" isbn="978-0-13-110362-7">
580
+ <title>K&amp;R C</title>
581
+ </book>
582
+ XML
583
+ end
584
+
585
+ it "generates a namespace class and uses it in the generated model" do
586
+ expect(defined?(RngNamespacedSpec::ComExampleBooksNamespace)).to eq("constant")
587
+ expect(defined?(RngNamespacedSpec::Book)).to eq("constant")
588
+ end
589
+
590
+ it "parses namespaced XML through the generated classes" do
591
+ book = RngNamespacedSpec::Book.from_xml(xml)
592
+ expect(book.isbn).to eq("978-0-13-110362-7")
593
+ expect(book.title).to eq("K&R C")
594
+ end
595
+
596
+ it "emits XML in the declared namespace on the root element" do
597
+ book = RngNamespacedSpec::Book.from_xml(xml)
598
+ expect(book.to_xml).to include('xmlns="http://example.com/books"')
599
+ end
600
+ end
601
+
602
+ context "e2e: mixed content loads + parses mixed XML" do
603
+ before do
604
+ stub_const("RngMixedSpec", Module.new)
605
+ described_class.to_models(
606
+ File.read("spec/fixtures/xml/schema/rng/paragraph.rng"),
607
+ output_dir: dir,
608
+ create_files: true,
609
+ module_namespace: "RngMixedSpec",
610
+ )
611
+ require File.join(dir, "rngmixedspec_registry.rb")
612
+ RngMixedSpec.register_all
613
+ end
614
+
615
+ after do
616
+ FileUtils.rm_rf(dir)
617
+ end
618
+
619
+ let(:dir) { Dir.mktmpdir }
620
+
621
+ it "loads a Para class with mixed_content enabled" do
622
+ expect(defined?(RngMixedSpec::Para)).to eq("constant")
623
+ para = RngMixedSpec::Para.from_xml(
624
+ "<para>Hello <emph>world</emph>!</para>",
625
+ )
626
+ expect(para).not_to be_nil
627
+ end
628
+ end
629
+
630
+ context "e2e: inline restrictions enforce constraints at parse time" do
631
+ before do
632
+ stub_const("RngInlineSpec", Module.new)
633
+ described_class.to_models(
634
+ rng,
635
+ output_dir: dir,
636
+ create_files: true,
637
+ module_namespace: "RngInlineSpec",
638
+ )
639
+ require File.join(dir, "rnginlinespec_registry.rb")
640
+ RngInlineSpec.register_all
641
+ end
642
+
643
+ after do
644
+ FileUtils.rm_rf(dir)
645
+ end
646
+
647
+ let(:dir) { Dir.mktmpdir }
648
+ let(:rng) do
649
+ <<~RNG
650
+ <grammar xmlns="http://relaxng.org/ns/structure/1.0">
651
+ <start>
652
+ <element name="game">
653
+ <attribute name="age">
654
+ <data type="integer">
655
+ <param name="minInclusive">3</param>
656
+ <param name="maxInclusive">18</param>
657
+ </data>
658
+ </attribute>
659
+ <element name="rating">
660
+ <choice>
661
+ <value>E</value>
662
+ <value>T</value>
663
+ <value>M</value>
664
+ </choice>
665
+ </element>
666
+ </element>
667
+ </start>
668
+ </grammar>
669
+ RNG
670
+ end
671
+
672
+ it "loads anonymous Type::Integer and Type::String subclasses" do
673
+ expect(defined?(RngInlineSpec::AgeType)).to eq("constant")
674
+ expect(defined?(RngInlineSpec::RatingType)).to eq("constant")
675
+ end
676
+
677
+ it "accepts XML satisfying the inline integer + enum restrictions" do
678
+ game = RngInlineSpec::Game.from_xml(
679
+ '<game age="12"><rating>T</rating></game>',
680
+ )
681
+ expect(game.age).to eq(12)
682
+ expect(game.rating).to eq("T")
683
+ end
684
+
685
+ it "rejects XML violating the inline integer minInclusive" do
686
+ expect do
687
+ RngInlineSpec::Game.from_xml('<game age="1"><rating>T</rating></game>')
688
+ end.to raise_error(Lutaml::Model::Type::MinBoundError)
689
+ end
690
+
691
+ it "rejects XML with a value outside the inline enumeration" do
692
+ expect do
693
+ RngInlineSpec::Game.from_xml(
694
+ '<game age="12"><rating>X</rating></game>',
695
+ )
696
+ end.to raise_error(Lutaml::Model::Type::InvalidValueError)
697
+ end
698
+ end
699
+
700
+ context "e2e: FullNameType (issue #9) loads + round-trips XML" do
701
+ before do
702
+ stub_const("RngFullNameSpec", Module.new)
703
+ described_class.to_models(
704
+ File.read("spec/fixtures/xml/schema/rng/full_name.rng"),
705
+ output_dir: dir,
706
+ create_files: true,
707
+ module_namespace: "RngFullNameSpec",
708
+ )
709
+ require File.join(dir, "rngfullnamespec_registry.rb")
710
+ RngFullNameSpec.register_all
711
+ end
712
+
713
+ after do
714
+ FileUtils.rm_rf(dir)
715
+ end
716
+
717
+ let(:dir) { Dir.mktmpdir }
718
+
719
+ it "loads the FullNameType class with all referenced defines" do
720
+ %w[FullNameType NameAbbreviation Prefix Forename FormattedInitials
721
+ Surname Addition CompleteName Biblionote Variantname].each do |c|
722
+ expect(RngFullNameSpec.const_defined?(c)).to be(true), "missing #{c}"
723
+ end
724
+ end
725
+
726
+ it "parses XML using the group/choice/zeroOrMore structure" do
727
+ xml = <<~XML
728
+ <fullname>
729
+ <abbrev>Dr.</abbrev>
730
+ <prefix>von</prefix>
731
+ <forename>Alice</forename>
732
+ <surname>Smith</surname>
733
+ <biblionote>note 1</biblionote>
734
+ </fullname>
735
+ XML
736
+ obj = RngFullNameSpec::FullNameType.from_xml(xml)
737
+ expect(obj.name_abbreviation.content).to eq("Dr.")
738
+ expect(obj.prefix.first.content).to eq("von")
739
+ expect(obj.forename.first.content).to eq("Alice")
740
+ expect(obj.surname.content).to eq("Smith")
741
+ expect(obj.biblionote.first.content).to eq("note 1")
742
+ end
743
+ end
744
+
745
+ context "e2e: union types load + cast across member types" do
746
+ before do
747
+ stub_const("RngUnionSpec", Module.new)
748
+ described_class.to_models(
749
+ File.read("spec/fixtures/xml/schema/rng/union.rng"),
750
+ output_dir: dir,
751
+ create_files: true,
752
+ module_namespace: "RngUnionSpec",
753
+ )
754
+ require File.join(dir, "rngunionspec_registry.rb")
755
+ RngUnionSpec.register_all
756
+ end
757
+
758
+ after do
759
+ FileUtils.rm_rf(dir)
760
+ end
761
+
762
+ let(:dir) { Dir.mktmpdir }
763
+
764
+ it "generates a Lutaml::Model::Type::Value subclass for the union define" do
765
+ expect(defined?(RngUnionSpec::IntOrString)).to eq("constant")
766
+ expect(RngUnionSpec::IntOrString.ancestors).to include(Lutaml::Model::Type::Value)
767
+ end
768
+
769
+ it "casts integer-shaped values to Integer" do
770
+ thing = RngUnionSpec::Thing.from_xml("<thing><count>42</count></thing>")
771
+ expect(thing.count).to eq(42)
772
+ end
773
+
774
+ it "casts non-integer values as String" do
775
+ thing = RngUnionSpec::Thing.from_xml("<thing><count>hello</count></thing>")
776
+ expect(thing.count).to eq("hello")
777
+ end
778
+ end
779
+
780
+ context "e2e: fixed-value attribute uses default + round-trips" do
781
+ before do
782
+ stub_const("RngFixedSpec", Module.new)
783
+ described_class.to_models(
784
+ File.read("spec/fixtures/xml/schema/rng/fixed_value.rng"),
785
+ output_dir: dir,
786
+ create_files: true,
787
+ module_namespace: "RngFixedSpec",
788
+ )
789
+ require File.join(dir, "rngfixedspec_registry.rb")
790
+ RngFixedSpec.register_all
791
+ end
792
+
793
+ after do
794
+ FileUtils.rm_rf(dir)
795
+ end
796
+
797
+ let(:dir) { Dir.mktmpdir }
798
+
799
+ it "applies the default value when the attribute is omitted from XML" do
800
+ thing = RngFixedSpec::Thing.from_xml("<thing><name>X</name></thing>")
801
+ expect(thing.version).to eq("1.0")
802
+ end
803
+
804
+ it "preserves an explicit attribute value" do
805
+ thing = RngFixedSpec::Thing.from_xml(
806
+ '<thing version="1.0"><name>X</name></thing>',
807
+ )
808
+ expect(thing.version).to eq("1.0")
809
+ end
810
+ end
811
+
812
+ context "e2e: load_classes: true (no files) loads classes into a tmp module" do
813
+ let!(:result) do
814
+ stub_const("RngLoadClassesSpec", Module.new)
815
+ described_class.to_models(
816
+ File.read("spec/fixtures/xml/schema/rng/address_book.rng"),
817
+ load_classes: true,
818
+ module_namespace: "RngLoadClassesSpec",
819
+ )
820
+ end
821
+
822
+ it "returns generated source per class" do
823
+ expect(result).to be_a(Hash)
824
+ expect(result.keys).to contain_exactly("AddressBook", "Card", "CardContent")
825
+ end
826
+
827
+ it "loads the namespaced module and registers classes" do
828
+ expect(defined?(RngLoadClassesSpec::AddressBook)).to eq("constant")
829
+ expect(defined?(RngLoadClassesSpec::Card)).to eq("constant")
830
+ end
831
+
832
+ it "the loaded classes can parse XML end-to-end" do
833
+ book = RngLoadClassesSpec::AddressBook.from_xml(
834
+ "<addressBook><card><name>X</name><email>x@y</email></card></addressBook>",
835
+ )
836
+ expect(book.card.first.name).to eq("X")
837
+ end
838
+ end
839
+
840
+ context "e2e: list type compiles + tolerates whitespace-separated XML" do
841
+ let(:dir) { Dir.mktmpdir }
842
+
843
+ before do
844
+ stub_const("RngListSpec", Module.new)
845
+ end
846
+
847
+ after do
848
+ FileUtils.rm_rf(dir)
849
+ end
850
+
851
+ it "compiles a grammar with <list> without crashing" do
852
+ expect do
853
+ described_class.to_models(
854
+ File.read("spec/fixtures/xml/schema/rng/list_type.rng"),
855
+ output_dir: dir,
856
+ create_files: true,
857
+ module_namespace: "RngListSpec",
858
+ )
859
+ require File.join(dir, "rnglistspec_registry.rb")
860
+ RngListSpec.register_all
861
+ end.not_to raise_error
862
+ expect(defined?(RngListSpec::Tags)).to eq("constant")
863
+ end
864
+ end
865
+
866
+ context "with a ref to a fragment define nested inside a choice" do
867
+ # Regression for the import-drop bug: handle_choice (and handle_group)
868
+ # collected imports onto a transient MemberCollector and then discarded
869
+ # them, so the generated container never emitted `import_model` /
870
+ # `require_relative` for the fragment ref.
871
+ let(:sources) do
872
+ described_class.to_models(
873
+ File.read("spec/fixtures/xml/schema/rng/fragment_in_choice.rng"),
874
+ )
875
+ end
876
+
877
+ it "emits import_model and require_relative for the fragment ref" do
878
+ expect(sources["Container"]).to include("import_model FragmentThing")
879
+ expect(sources["Container"]).to include(%(require_relative "fragment_thing"))
880
+ end
881
+ end
882
+
883
+ context "via Schema.from_relaxng entry point" do
884
+ it "delegates through the registered method" do
885
+ result = Lutaml::Model::Schema.from_relaxng(
886
+ File.read("spec/fixtures/xml/schema/rng/address_book.rng"),
887
+ )
888
+ expect(result).to be_a(Hash)
889
+ expect(result.keys).to contain_exactly("AddressBook", "Card", "CardContent")
890
+ end
891
+ end
892
+ end
893
+ end