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,404 @@
1
+ require "rng"
2
+ require "spec_helper"
3
+ require "lutaml/model/schema"
4
+ require "tmpdir"
5
+
6
+ RSpec.describe Lutaml::Model::Schema::RncCompiler do
7
+ before do
8
+ default_ctx = Lutaml::Model::GlobalContext.context(:default)
9
+ if default_ctx
10
+ default_ctx.registry.clear
11
+ Lutaml::Model::Type.register_builtin_types_in(default_ctx.registry)
12
+ end
13
+ end
14
+
15
+ describe ".to_models" do
16
+ context "with an RNC schema using named patterns and start |=" do
17
+ let(:schema) { File.read("spec/fixtures/xml/schema/rnc/address_book.rnc") }
18
+ let(:sources) { described_class.to_models(schema) }
19
+
20
+ it "compiles RNC through the RNG compiler" do
21
+ expect(sources.keys).to contain_exactly(
22
+ "AddressBook", "Card", "Name", "Email"
23
+ )
24
+ expect(sources["AddressBook"]).to include("class AddressBook")
25
+ expect(sources["AddressBook"]).to include('map_element "card", to: :card')
26
+ end
27
+
28
+ it "does not emit a synthetic Start class for the RNC start pattern" do
29
+ expect(sources).not_to have_key("Start")
30
+ end
31
+ end
32
+
33
+ context "with an inline start containing a named reference" do
34
+ let(:schema) do
35
+ <<~RNC
36
+ item = element item { text }
37
+ start = element root { item* }
38
+ RNC
39
+ end
40
+
41
+ it "generates the root model, not just the referenced child" do
42
+ sources = described_class.to_models(schema)
43
+
44
+ expect(sources.keys).to contain_exactly("Root", "Item")
45
+ expect(sources["Root"]).to include("class Root")
46
+ expect(sources["Root"]).to include('map_element "item", to: :item')
47
+ end
48
+ end
49
+
50
+ context "with a ref-only inline start defined after its target" do
51
+ let(:schema) do
52
+ <<~RNC
53
+ library = element library { text }
54
+ start = library
55
+ RNC
56
+ end
57
+
58
+ it "roots the referenced define without emitting a Start class" do
59
+ sources = described_class.to_models(schema)
60
+
61
+ expect(sources.keys).to include("Library")
62
+ expect(sources).not_to have_key("Start")
63
+ end
64
+ end
65
+
66
+ context "with create_files output" do
67
+ let(:dir) { Dir.mktmpdir }
68
+ let(:schema) { File.read("spec/fixtures/xml/schema/rnc/address_book.rnc") }
69
+
70
+ before do
71
+ stub_const("RncAddressBookSpec", Module.new)
72
+ described_class.to_models(
73
+ schema,
74
+ output_dir: dir,
75
+ create_files: true,
76
+ module_namespace: "RncAddressBookSpec",
77
+ )
78
+ require File.join(dir, "rncaddressbookspec_registry.rb")
79
+ RncAddressBookSpec.register_all
80
+ end
81
+
82
+ after do
83
+ FileUtils.rm_rf(dir)
84
+ end
85
+
86
+ it "writes namespaced files and registry output" do
87
+ module_dir = File.join(dir, "rncaddressbookspec")
88
+ expect(File).to exist(File.join(dir, "rncaddressbookspec_registry.rb"))
89
+ expect(File).to exist(File.join(module_dir, "address_book.rb"))
90
+ expect(File).to exist(File.join(module_dir, "card.rb"))
91
+ end
92
+
93
+ it "loads generated classes that can parse XML" do
94
+ xml = <<~XML
95
+ <addressBook>
96
+ <card>
97
+ <name>Alice</name>
98
+ <email>alice@example.com</email>
99
+ </card>
100
+ </addressBook>
101
+ XML
102
+
103
+ book = RncAddressBookSpec::AddressBook.from_xml(xml)
104
+ expect(book.card.first.name.content).to eq("Alice")
105
+ expect(book.card.first.email.content).to eq("alice@example.com")
106
+ end
107
+ end
108
+
109
+ context "with RNC-specific syntax used by RFC XML schemas" do
110
+ let(:sources) do
111
+ described_class.to_models(
112
+ File.read("spec/fixtures/xml/schema/rnc/book_features.rnc"),
113
+ )
114
+ end
115
+
116
+ it "accepts compatibility annotations before attributes" do
117
+ expect(sources["Book"]).to include("class Book")
118
+ expect(sources["Book"]).to include('map_attribute "format", to: :format')
119
+ end
120
+
121
+ it "treats an attribute choice mixing text with a fixed value as a string" do
122
+ expect(sources["Book"]).to include("attribute :indent, :string")
123
+ expect(sources["Book"]).to include('map_attribute "indent", to: :indent')
124
+ expect(sources.keys).not_to include("IndentType")
125
+ end
126
+
127
+ it "preserves xml-prefixed attributes in the XML mapping" do
128
+ expect(sources["Book"]).to include("attribute :xml_lang")
129
+ expect(sources["Book"]).to include('map_attribute "xml:lang", to: :xml_lang')
130
+ end
131
+
132
+ it "normalizes escaped identifiers into generated class names" do
133
+ expect(sources.keys).to include("List")
134
+ expect(sources["Book"]).to include("attribute :list, List")
135
+ end
136
+
137
+ it "loads generated classes that parse normalized RNC mappings" do
138
+ stub_const("RncFeatureSpec", Module.new)
139
+ described_class.to_models(
140
+ File.read("spec/fixtures/xml/schema/rnc/book_features.rnc"),
141
+ load_classes: true,
142
+ module_namespace: "RncFeatureSpec",
143
+ )
144
+
145
+ xml = <<~XML
146
+ <book xml:lang="en" indent="adaptive">
147
+ <title>Example</title>
148
+ <list>
149
+ <item>First</item>
150
+ <item>Second</item>
151
+ </list>
152
+ </book>
153
+ XML
154
+
155
+ book = RncFeatureSpec::Book.from_xml(xml)
156
+ expect(book.xml_lang).to eq("en")
157
+ expect(book.indent).to eq("adaptive")
158
+ expect(book.title.content).to eq("Example")
159
+ expect(book.list.item.map(&:content)).to eq(%w[First Second])
160
+ end
161
+
162
+ it "does not rewrite attributes with nested datatype parameter braces" do
163
+ source = <<~RNC
164
+ root =
165
+ element root {
166
+ attribute code { xsd:string { pattern = "[A-Z]+" } }
167
+ }
168
+
169
+ start |= root
170
+ RNC
171
+
172
+ sources = described_class.to_models(source)
173
+
174
+ expect(sources.keys).to include("Root", "CodeType")
175
+ expect(sources["Root"]).to include("attribute :code, :code_type")
176
+ # The `[A-Z]` inside the string literal must survive: if annotation
177
+ # stripping wrongly treated `[` in the string as a bracket annotation,
178
+ # the pattern would be corrupted.
179
+ expect(sources["CodeType"]).to include("[A-Z]+")
180
+ end
181
+ end
182
+
183
+ context "with include resolution through the location option" do
184
+ let(:path) do
185
+ "spec/fixtures/xml/schema/rnc/includes/library.rnc"
186
+ end
187
+
188
+ it "loads included RNC definitions before delegating to RngCompiler" do
189
+ sources = described_class.to_models(File.read(path), location: path)
190
+
191
+ expect(sources.keys).to include("Library", "Book", "Title")
192
+ expect(sources["Library"]).to include("attribute :book, Book")
193
+ end
194
+
195
+ it "raises a clear error for missing include files" do
196
+ expect do
197
+ described_class.to_models(
198
+ 'include "missing.rnc"',
199
+ location: "spec/fixtures/xml/schema/rnc/includes",
200
+ )
201
+ end.to raise_error(/missing\.rnc/)
202
+ end
203
+
204
+ it "raises a clear error for circular includes" do
205
+ circular_path =
206
+ "spec/fixtures/xml/schema/rnc/includes/circular_a.rnc"
207
+
208
+ expect do
209
+ described_class.to_models(File.read(circular_path), location: circular_path)
210
+ end.to raise_error(/circular_a\.rnc/)
211
+ end
212
+ end
213
+
214
+ context "with load_classes mode" do
215
+ let!(:sources) do
216
+ stub_const("RncLoadClassesSpec", Module.new)
217
+ described_class.to_models(
218
+ File.read("spec/fixtures/xml/schema/rnc/address_book.rnc"),
219
+ load_classes: true,
220
+ module_namespace: "RncLoadClassesSpec",
221
+ )
222
+ end
223
+
224
+ it "returns generated source and loads constants" do
225
+ expect(sources.keys).to contain_exactly(
226
+ "AddressBook", "Card", "Name", "Email"
227
+ )
228
+ expect(defined?(RncLoadClassesSpec::AddressBook)).to eq("constant")
229
+ end
230
+ end
231
+
232
+ context "with input text and a file location" do
233
+ # Regression for the SourceResolver override bug: when caller passed
234
+ # both `input` and `location:` pointing at a file, the resolver
235
+ # silently read the file from disk and discarded the input.
236
+ it "uses the caller-provided text, not the file contents" do
237
+ Dir.mktmpdir do |dir|
238
+ path = File.join(dir, "real_file.rnc")
239
+ File.write(path, "start = element from_disk { text }")
240
+ inline = "start = element from_inline { text }"
241
+
242
+ sources = described_class.to_models(inline, location: path)
243
+
244
+ expect(sources.keys).to include("FromInline")
245
+ expect(sources.keys).not_to include("FromDisk")
246
+ end
247
+ end
248
+ end
249
+
250
+ context "with an attribute in a foreign namespace" do
251
+ let(:source) do
252
+ %(namespace ex = "urn:test"\n) +
253
+ %(start = element root { attribute ex:code { text } })
254
+ end
255
+
256
+ it "generates a namespaced value type for the attribute" do
257
+ sources = described_class.to_models(source)
258
+
259
+ code_type = sources.keys.find { |k| k.include?("Type") }
260
+ expect(code_type).not_to be_nil
261
+ expect(sources[code_type]).to match(/namespace \w+Namespace/)
262
+ expect(sources[code_type]).to match(/require_relative ".*namespace"/)
263
+ expect(sources["Root"]).to include('map_attribute "code", to: :code')
264
+ end
265
+
266
+ it "round-trips the foreign-namespace attribute (parse + fresh serialize)" do
267
+ stub_const("RncForeignNs", Module.new)
268
+ described_class.to_models(
269
+ source, load_classes: true, module_namespace: "RncForeignNs"
270
+ )
271
+
272
+ parsed = RncForeignNs::Root.from_xml(%(<root xmlns:ex="urn:test" ex:code="q"/>))
273
+ expect(parsed.code).to eq("q")
274
+
275
+ # The attribute round-trips in its namespace; the emitted prefix is a
276
+ # generated one (rng does not expose the source prefix), so match the
277
+ # URI and a namespace-qualified `code` attribute rather than "ex".
278
+ out = RncForeignNs::Root.new(code: "f").to_xml
279
+ expect(out).to match(/xmlns:\w+="urn:test"/)
280
+ expect(out).to match(/\w+:code="f"/)
281
+ end
282
+
283
+ it "does not namespace a type shared with an unqualified member" do
284
+ sources = described_class.to_models(<<~RNC)
285
+ namespace ex = "urn:test"
286
+ codeType = xsd:string { maxLength = "5" }
287
+ start = element root {
288
+ attribute plain { codeType },
289
+ attribute ex:tagged { codeType }
290
+ }
291
+ RNC
292
+
293
+ # The shared codeType must NOT gain a namespace (plain uses it
294
+ # unqualified); the foreign attribute gets its own namespaced type.
295
+ expect(sources["CodeType"]).not_to match(/namespace \w+Namespace/)
296
+ namespaced = sources.keys.find do |k|
297
+ k != "DefaultNamespace" && sources[k].match?(/namespace \w+Namespace/)
298
+ end
299
+ expect(namespaced).not_to be_nil
300
+ end
301
+
302
+ it "namespaces a foreign-ns attribute with inline facets via a subclass" do
303
+ sources = described_class.to_models(
304
+ %(namespace ex = "urn:test"\n) +
305
+ %(start = element root { attribute ex:code { xsd:string { maxLength = "5" } } }),
306
+ )
307
+
308
+ # The inline restricted type is subclassed (not mutated) so a fresh
309
+ # namespaced type inherits its constraints and carries the namespace.
310
+ expect(sources["CodeType"]).not_to match(/namespace \w+Namespace/)
311
+ subclass = sources.keys.find { |k| sources[k].match?(/class \w+ < CodeType/) }
312
+ expect(subclass).not_to be_nil
313
+ expect(sources[subclass]).to match(/namespace \w+Namespace/)
314
+ end
315
+
316
+ it "keeps distinct URIs and prefixes when their generated names collide" do
317
+ sources = described_class.to_models(<<~RNC)
318
+ namespace x = "urn:a"
319
+ namespace y = "urn:b"
320
+ start = element root { attribute x:one { text }, attribute y:two { text } }
321
+ RNC
322
+
323
+ namespaces = sources.select { |_, v| v.include?("Lutaml::Xml::W3c::XmlNamespace") }
324
+ expect(namespaces.size).to eq(2)
325
+ uris = namespaces.values.map { |v| v[/uri "([^"]+)"/, 1] }
326
+ prefixes = namespaces.values.map { |v| v[/prefix_default "([^"]+)"/, 1] }
327
+ expect(uris).to contain_exactly("urn:a", "urn:b")
328
+ expect(prefixes.uniq.size).to eq(2)
329
+ end
330
+
331
+ it "does not let a define shadow a built-in type of the same name" do
332
+ # `element pre { dateTime }` forces the custom `dateTime` define into
333
+ # the class set BEFORE the foreign-ns attribute resolves, so the guard
334
+ # is actually exercised: the attribute's primitive xsd:dateTime must
335
+ # still subclass the built-in DateTime, not the custom `dateTime`.
336
+ sources = described_class.to_models(<<~RNC)
337
+ dateTime = xsd:string { pattern = "custom" }
338
+ start = element root {
339
+ element pre { dateTime },
340
+ attribute ex:when { xsd:dateTime }
341
+ }
342
+ RNC
343
+
344
+ wrapper = sources.keys.find { |k| sources[k].match?(/namespace \w+Namespace/) && !k.include?("Namespace") }
345
+ expect(sources[wrapper]).to include("< Lutaml::Model::Type::DateTime")
346
+ end
347
+ end
348
+
349
+ context "with namespaced root and child elements" do
350
+ let(:source) do
351
+ %(namespace ex = "urn:test"\n) +
352
+ %(start = element ex:root { element ex:child { text } })
353
+ end
354
+
355
+ it "declares a class-level namespace with qualified element form" do
356
+ sources = described_class.to_models(source)
357
+ ns_key = sources.keys.find { |k| k.include?("Namespace") }
358
+
359
+ expect(sources["Root"]).to match(/namespace \w+Namespace/)
360
+ expect(sources[ns_key]).to include('uri "urn:test"')
361
+ expect(sources[ns_key]).to include("element_form_default :qualified")
362
+ end
363
+
364
+ it "round-trips root and child in their namespace" do
365
+ stub_const("RncNsElem", Module.new)
366
+ described_class.to_models(
367
+ source, load_classes: true, module_namespace: "RncNsElem"
368
+ )
369
+
370
+ xml = %(<ex:root xmlns:ex="urn:test"><ex:child>hi</ex:child></ex:root>)
371
+ expect(RncNsElem::Root.from_xml(xml).child).to eq("hi")
372
+
373
+ out = RncNsElem::Root.new(child: "x").to_xml
374
+ expect(out).to match(/xmlns(:\w+)?="urn:test"/)
375
+ expect(out).not_to include('xmlns=""')
376
+ end
377
+ end
378
+
379
+ context "with bracket annotations handled natively by rng" do
380
+ it "compiles an annotation before a non-leading definition" do
381
+ sources = described_class.to_models(<<~RNC)
382
+ foo = element foo { text }
383
+ [ a:defaultValue = "x" ]
384
+ bar = element bar { text }
385
+ start = element root { foo, bar }
386
+ RNC
387
+
388
+ expect(sources.keys).to include("Root", "Foo", "Bar")
389
+ end
390
+ end
391
+
392
+ context "via Schema.from_rnc entry point" do
393
+ it "delegates through the registered XML schema method" do
394
+ result = Lutaml::Model::Schema.from_rnc(
395
+ File.read("spec/fixtures/xml/schema/rnc/address_book.rnc"),
396
+ )
397
+
398
+ expect(result.keys).to contain_exactly(
399
+ "AddressBook", "Card", "Name", "Email"
400
+ )
401
+ end
402
+ end
403
+ end
404
+ end