lutaml-model 0.8.16 → 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 (230) 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/.github/workflows/release.yml +0 -3
  9. data/.gitmodules +6 -0
  10. data/.rubocop_todo.yml +76 -12
  11. data/Gemfile +16 -1
  12. data/README.adoc +267 -0
  13. data/Rakefile +166 -4
  14. data/docs/_guides/schema-import.adoc +42 -0
  15. data/docs/_guides/xml/namespace-semantics.adoc +2 -0
  16. data/docs/_guides/xml-mapping.adoc +21 -0
  17. data/docs/_guides/xml-namespace-qualification.adoc +142 -0
  18. data/docs/_pages/validation.adoc +65 -0
  19. data/docs/_tutorials/xml-element-attribute-namespace-guide.adoc +2 -0
  20. data/docs/_tutorials/xml-schema-primer-style-guide.adoc +2 -0
  21. data/docs/namespace-management.adoc +2 -0
  22. data/docs/xml-schema-qualification.md +6 -0
  23. data/lib/compat/opal/generate_boot.rb +123 -0
  24. data/lib/compat/opal/js_bundle_entry.rb +42 -0
  25. data/lib/compat/opal/lutaml_model_boot.rb +497 -0
  26. data/lib/compat/opal/moxml_boot.rb +68 -0
  27. data/lib/compat/opal/yaml_compat.rb +32 -0
  28. data/lib/lutaml/json.rb +1 -1
  29. data/lib/lutaml/jsonld.rb +1 -4
  30. data/lib/lutaml/key_value/transform.rb +3 -8
  31. data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -1
  32. data/lib/lutaml/key_value/transformation.rb +17 -5
  33. data/lib/lutaml/model/adapter_resolver.rb +2 -2
  34. data/lib/lutaml/model/attribute.rb +88 -11
  35. data/lib/lutaml/model/cached_type_resolver.rb +1 -1
  36. data/lib/lutaml/model/choice.rb +34 -0
  37. data/lib/lutaml/model/compiled_rule.rb +7 -0
  38. data/lib/lutaml/model/error/union_schema_unsupported_error.rb +11 -0
  39. data/lib/lutaml/model/global_context.rb +11 -8
  40. data/lib/lutaml/model/instrumentation.rb +2 -2
  41. data/lib/lutaml/model/mapping/mapping_rule.rb +0 -6
  42. data/lib/lutaml/model/register.rb +2 -2
  43. data/lib/lutaml/model/runtime_compatibility.rb +50 -0
  44. data/lib/lutaml/model/schema/class_loader.rb +58 -0
  45. data/lib/lutaml/model/schema/compiled_output.rb +83 -0
  46. data/lib/lutaml/model/schema/definitions/attribute.rb +32 -0
  47. data/lib/lutaml/model/schema/definitions/choice.rb +20 -0
  48. data/lib/lutaml/model/schema/definitions/facet.rb +40 -0
  49. data/lib/lutaml/model/schema/definitions/group_import.rb +22 -0
  50. data/lib/lutaml/model/schema/definitions/member_walk.rb +33 -0
  51. data/lib/lutaml/model/schema/definitions/model.rb +40 -0
  52. data/lib/lutaml/model/schema/definitions/namespace.rb +23 -0
  53. data/lib/lutaml/model/schema/definitions/restricted_type.rb +30 -0
  54. data/lib/lutaml/model/schema/definitions/sequence.rb +19 -0
  55. data/lib/lutaml/model/schema/definitions/simple_content.rb +22 -0
  56. data/lib/lutaml/model/schema/definitions/transform_facet.rb +21 -0
  57. data/lib/lutaml/model/schema/definitions/type_ref.rb +20 -0
  58. data/lib/lutaml/model/schema/definitions/union_type.rb +28 -0
  59. data/lib/lutaml/model/schema/definitions/xml_root.rb +20 -0
  60. data/lib/lutaml/model/schema/definitions.rb +24 -0
  61. data/lib/lutaml/model/schema/file_writer.rb +70 -0
  62. data/lib/lutaml/model/schema/generator/definitions_collection.rb +16 -2
  63. data/lib/lutaml/model/schema/generator/property.rb +21 -4
  64. data/lib/lutaml/model/schema/module_nesting.rb +31 -0
  65. data/lib/lutaml/model/schema/namespace_naming.rb +46 -0
  66. data/lib/lutaml/model/schema/registry_generator.rb +115 -0
  67. data/lib/lutaml/model/schema/renderers/base.rb +40 -0
  68. data/lib/lutaml/model/schema/renderers/mappings.rb +72 -0
  69. data/lib/lutaml/model/schema/renderers/member_decls.rb +115 -0
  70. data/lib/lutaml/model/schema/renderers/model.rb +121 -0
  71. data/lib/lutaml/model/schema/renderers/namespace.rb +30 -0
  72. data/lib/lutaml/model/schema/renderers/registration.rb +64 -0
  73. data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +93 -0
  74. data/lib/lutaml/model/schema/renderers/restricted_type.rb +88 -0
  75. data/lib/lutaml/model/schema/renderers/union.rb +83 -0
  76. data/lib/lutaml/model/schema/renderers.rb +19 -0
  77. data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +47 -0
  78. data/lib/lutaml/model/schema/rnc_compiler.rb +62 -0
  79. data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +91 -0
  80. data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +392 -0
  81. data/lib/lutaml/model/schema/rng_compiler/member_collector.rb +30 -0
  82. data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +152 -0
  83. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +125 -0
  84. data/lib/lutaml/model/schema/rng_compiler.rb +220 -0
  85. data/lib/lutaml/model/schema/templates.rb +162 -0
  86. data/lib/lutaml/model/schema/xml_compiler/element_order.rb +41 -0
  87. data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +31 -107
  88. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +191 -0
  89. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +216 -0
  90. data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +153 -0
  91. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +167 -0
  92. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +87 -0
  93. data/lib/lutaml/model/schema/xml_compiler.rb +38 -517
  94. data/lib/lutaml/model/schema.rb +19 -0
  95. data/lib/lutaml/model/serialize/attribute_definition.rb +14 -2
  96. data/lib/lutaml/model/serialize/builder.rb +15 -2
  97. data/lib/lutaml/model/serialize/value_mapping.rb +9 -57
  98. data/lib/lutaml/model/serialize.rb +6 -9
  99. data/lib/lutaml/model/services/rule_value_extractor.rb +2 -3
  100. data/lib/lutaml/model/services.rb +0 -2
  101. data/lib/lutaml/model/store.rb +2 -1
  102. data/lib/lutaml/model/toml.rb +1 -1
  103. data/lib/lutaml/model/transform.rb +17 -39
  104. data/lib/lutaml/model/transformation_registry.rb +1 -1
  105. data/lib/lutaml/model/type.rb +1 -0
  106. data/lib/lutaml/model/union.rb +320 -0
  107. data/lib/lutaml/model/utils.rb +6 -0
  108. data/lib/lutaml/model/validation.rb +1 -4
  109. data/lib/lutaml/model/version.rb +1 -1
  110. data/lib/lutaml/model.rb +19 -0
  111. data/lib/lutaml/{jsonld → rdf}/context.rb +1 -1
  112. data/lib/lutaml/{jsonld/transform.rb → rdf/linked_data_transform.rb} +33 -35
  113. data/lib/lutaml/{jsonld → rdf}/term_definition.rb +1 -1
  114. data/lib/lutaml/rdf.rb +3 -0
  115. data/lib/lutaml/toml.rb +1 -1
  116. data/lib/lutaml/xml/adapter.rb +1 -1
  117. data/lib/lutaml/xml/adapter_element.rb +2 -2
  118. data/lib/lutaml/xml/adapter_loader.rb +1 -1
  119. data/lib/lutaml/xml/builder/base.rb +1 -1
  120. data/lib/lutaml/xml/builder.rb +1 -1
  121. data/lib/lutaml/xml/error/schema_validation_error.rb +25 -0
  122. data/lib/lutaml/xml/model_transform.rb +5 -1
  123. data/lib/lutaml/xml/schema/relaxng_schema.rb +5 -0
  124. data/lib/lutaml/xml/schema/xsd_schema.rb +4 -0
  125. data/lib/lutaml/xml/serialization/format_conversion.rb +48 -0
  126. data/lib/lutaml/xml/serialization/instance_methods.rb +13 -0
  127. data/lib/lutaml/xml/transformation/element_builder.rb +59 -31
  128. data/lib/lutaml/xml/transformation/ordered_applier.rb +6 -0
  129. data/lib/lutaml/xml/xsd_validator.rb +66 -0
  130. data/lib/lutaml/xml.rb +53 -12
  131. data/lib/lutaml/yamlld/adapter.rb +25 -0
  132. data/lib/lutaml/yamlld.rb +25 -0
  133. data/lutaml-model.gemspec +4 -2
  134. data/spec/fixtures/xml/schema/rnc/address_book.rnc +15 -0
  135. data/spec/fixtures/xml/schema/rnc/book_features.rnc +18 -0
  136. data/spec/fixtures/xml/schema/rnc/includes/book.rnc +6 -0
  137. data/spec/fixtures/xml/schema/rnc/includes/circular_a.rnc +8 -0
  138. data/spec/fixtures/xml/schema/rnc/includes/circular_b.rnc +8 -0
  139. data/spec/fixtures/xml/schema/rnc/includes/library.rnc +8 -0
  140. data/spec/fixtures/xml/schema/rng/address_book.rng +16 -0
  141. data/spec/fixtures/xml/schema/rng/book.rng +27 -0
  142. data/spec/fixtures/xml/schema/rng/fixed_value.rng +11 -0
  143. data/spec/fixtures/xml/schema/rng/fragment_in_choice.rng +30 -0
  144. data/spec/fixtures/xml/schema/rng/full_name.rng +39 -0
  145. data/spec/fixtures/xml/schema/rng/integer_range.rng +32 -0
  146. data/spec/fixtures/xml/schema/rng/list_type.rng +12 -0
  147. data/spec/fixtures/xml/schema/rng/namespaced.rng +9 -0
  148. data/spec/fixtures/xml/schema/rng/paragraph.rng +13 -0
  149. data/spec/fixtures/xml/schema/rng/person.rng +23 -0
  150. data/spec/fixtures/xml/schema/rng/union.rng +14 -0
  151. data/spec/fixtures/xml/validate_xml_with_address.xsd +13 -0
  152. data/spec/fixtures/xml/validate_xml_with_contact.xsd +11 -0
  153. data/spec/fixtures/xml/validate_xml_with_person.xsd +11 -0
  154. data/spec/fixtures/xml/validate_xml_with_person_strict.xsd +17 -0
  155. data/spec/fixtures/xml/validate_xml_with_product.xsd +37 -0
  156. data/spec/lutaml/integration/multi_format_spec.rb +23 -0
  157. data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
  158. data/spec/lutaml/model/attribute_spec.rb +177 -13
  159. data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
  160. data/spec/lutaml/model/choice_restrict_spec.rb +225 -0
  161. data/spec/lutaml/model/collection_spec.rb +18 -0
  162. data/spec/lutaml/model/custom_model_spec.rb +4 -4
  163. data/spec/lutaml/model/mixed_content_spec.rb +173 -2
  164. data/spec/lutaml/model/multiple_mapping_spec.rb +4 -4
  165. data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
  166. data/spec/lutaml/model/ordered_content_spec.rb +33 -3
  167. data/spec/lutaml/model/raw_element_spec.rb +125 -0
  168. data/spec/lutaml/model/rule_value_extractor_spec.rb +7 -14
  169. data/spec/lutaml/model/runtime_compatibility_spec.rb +25 -0
  170. data/spec/lutaml/model/schema/renderers/model_spec.rb +149 -0
  171. data/spec/lutaml/model/schema/renderers/namespace_spec.rb +36 -0
  172. data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +75 -0
  173. data/spec/lutaml/model/schema/renderers/union_spec.rb +58 -0
  174. data/spec/lutaml/model/transform_apply_value_map_spec.rb +64 -0
  175. data/spec/lutaml/model/uninitialized_class_spec.rb +1 -1
  176. data/spec/lutaml/model/union_attribute_spec.rb +658 -0
  177. data/spec/lutaml/model/union_spec.rb +257 -0
  178. data/spec/lutaml/model/utils_spec.rb +14 -0
  179. data/spec/lutaml/model/xsd_form_default_patterns_spec.rb +2 -2
  180. data/spec/lutaml/model/xsd_patterns_spec.rb +4 -4
  181. data/spec/lutaml/{jsonld → rdf}/context_spec.rb +2 -2
  182. data/spec/lutaml/{jsonld/transform_spec.rb → rdf/linked_data_transform_spec.rb} +15 -1
  183. data/spec/lutaml/{jsonld → rdf}/term_definition_spec.rb +2 -2
  184. data/spec/lutaml/turtle/transform_spec.rb +2 -2
  185. data/spec/lutaml/xml/enhanced_mapping_spec.rb +230 -1
  186. data/spec/lutaml/xml/mapping_spec.rb +4 -4
  187. data/spec/lutaml/xml/namespace_placement_spec.rb +11 -8
  188. data/spec/lutaml/xml/namespace_three_phase_spec.rb +1 -1
  189. data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
  190. data/spec/lutaml/xml/prefix_control_spec.rb +4 -4
  191. data/spec/lutaml/xml/schema/compiler_spec.rb +21 -4
  192. data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +404 -0
  193. data/spec/lutaml/xml/schema/rng_compiler_spec.rb +893 -0
  194. data/spec/lutaml/xml/serializable_namespace_spec.rb +6 -6
  195. data/spec/lutaml/xml/type_namespace_examples_spec.rb +1 -1
  196. data/spec/lutaml/xml/type_namespace_integration_spec.rb +3 -3
  197. data/spec/lutaml/xml/validate_xml_with_constructs_spec.rb +222 -0
  198. data/spec/lutaml/xml/validate_xml_with_spec.rb +186 -0
  199. data/spec/lutaml/yamlld/adapter_spec.rb +56 -0
  200. data/spec/lutaml/yamlld/registration_spec.rb +33 -0
  201. data/spec/spec_helper.rb +28 -11
  202. data/spec/support/opal.rb +5 -2
  203. metadata +108 -37
  204. data/lib/lutaml/model/schema/xml_compiler/attribute.rb +0 -106
  205. data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +0 -47
  206. data/lib/lutaml/model/schema/xml_compiler/choice.rb +0 -67
  207. data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +0 -27
  208. data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +0 -34
  209. data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +0 -173
  210. data/lib/lutaml/model/schema/xml_compiler/element.rb +0 -121
  211. data/lib/lutaml/model/schema/xml_compiler/group.rb +0 -113
  212. data/lib/lutaml/model/schema/xml_compiler/restriction.rb +0 -104
  213. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -52
  214. data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +0 -40
  215. data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +0 -265
  216. data/lib/lutaml/model/schema/xml_compiler/xml_namespace_class.rb +0 -110
  217. data/lib/lutaml/model/services/default_value_resolver.rb +0 -60
  218. data/spec/lutaml/model/services/default_value_resolver_spec.rb +0 -162
  219. data/spec/lutaml/xml/schema/compiler/attribute_group_spec.rb +0 -71
  220. data/spec/lutaml/xml/schema/compiler/attribute_spec.rb +0 -67
  221. data/spec/lutaml/xml/schema/compiler/choice_spec.rb +0 -74
  222. data/spec/lutaml/xml/schema/compiler/complex_content_restriction_spec.rb +0 -60
  223. data/spec/lutaml/xml/schema/compiler/complex_content_spec.rb +0 -41
  224. data/spec/lutaml/xml/schema/compiler/complex_type_spec.rb +0 -202
  225. data/spec/lutaml/xml/schema/compiler/element_spec.rb +0 -68
  226. data/spec/lutaml/xml/schema/compiler/group_spec.rb +0 -90
  227. data/spec/lutaml/xml/schema/compiler/restriction_spec.rb +0 -77
  228. data/spec/lutaml/xml/schema/compiler/sequence_spec.rb +0 -65
  229. data/spec/lutaml/xml/schema/compiler/simple_content_spec.rb +0 -61
  230. data/spec/lutaml/xml/schema/compiler/simple_type_spec.rb +0 -184
@@ -91,18 +91,21 @@ RSpec.describe "XML Namespace Placement" do
91
91
  end
92
92
 
93
93
  # This is the INCORRECT pattern - namespace at class level for Models
94
- broken_model = Class.new do
95
- include Lutaml::Model::Serialize
94
+ broken_model = nil
95
+ expect do
96
+ broken_model = Class.new do
97
+ include Lutaml::Model::Serialize
96
98
 
97
- namespace ns # ❌ This doesn't work for Serializable classes!
99
+ namespace ns # ❌ This doesn't work for Serializable classes!
98
100
 
99
- attribute :value, :string
101
+ attribute :value, :string
100
102
 
101
- xml do
102
- element "broken"
103
- map_content to: :value
103
+ xml do
104
+ element "broken"
105
+ map_content to: :value
106
+ end
104
107
  end
105
- end
108
+ end.to output(/DEPRECATION WARNING.*Class-level.*namespace/).to_stderr
106
109
 
107
110
  parent_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
108
111
  uri "http://example.com/parent"
@@ -251,7 +251,7 @@ RSpec.describe "Three-phase namespace algorithm" do
251
251
  expect(xml).to include("<version>4.0</version>")
252
252
 
253
253
  # Should NOT add redundant prefix
254
- expect(xml).not_to match(/<vcard:version>/)
254
+ expect(xml).not_to include("<vcard:version>")
255
255
  end
256
256
  end
257
257
  end
@@ -2,9 +2,13 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- RSpec.describe "XML with REXML under Opal", if: RUBY_ENGINE == "opal" do
5
+ # Adapter-agnostic XML round-trip scenarios, shared between Oga and
6
+ # REXML smoke runs. Both adapters are pure Ruby and work under Opal;
7
+ # this shared example is the contract that any future Opal-capable
8
+ # adapter would also need to satisfy.
9
+ RSpec.shared_examples "opal XML adapter round-trip" do |adapter_name|
6
10
  before do
7
- Lutaml::Model::Config.xml_adapter_type = :rexml
11
+ Lutaml::Model::Config.xml_adapter_type = adapter_name
8
12
  end
9
13
 
10
14
  it "round-trips XML through parse and serialize" do
@@ -118,28 +122,26 @@ RSpec.describe "XML with REXML under Opal", if: RUBY_ENGINE == "opal" do
118
122
  expect(xml).to include("<title>Widget</title>")
119
123
  expect(xml).to include("<count>5</count>")
120
124
  end
125
+ end
121
126
 
122
- it "handles XmlOrderable on plain model classes" do
123
- plain_model = Class.new do
124
- attr_accessor :data
125
- end
127
+ # Pure-Ruby XML adapters that ship in the Opal bundle. Each nested
128
+ # group runs the shared contract for its adapter; the top-level guard
129
+ # gates the whole file under Opal.
130
+ RSpec.describe "XML adapters under Opal", if: RUBY_ENGINE == "opal" do
131
+ describe "with Oga (default)" do
132
+ it_behaves_like "opal XML adapter round-trip", :oga
126
133
 
127
- Class.new do
128
- include Lutaml::Model::Serialize
129
-
130
- const_set(:MODEL_CLASS, plain_model)
131
- model plain_model
132
- attribute :data, :string
133
- xml do
134
- element "data"
135
- map_element "data", to: :data
136
- end
134
+ it "uses Oga adapter by default under Opal" do
135
+ expect(Lutaml::Model::Config.xml_adapter_type).to eq(:oga)
137
136
  end
138
-
139
- expect(plain_model.ancestors).to include(Lutaml::Xml::XmlOrderable)
140
137
  end
141
138
 
142
- it "uses REXML adapter by default under Opal" do
143
- expect(Lutaml::Model::Config.xml_adapter_type).to eq(:rexml)
139
+ describe "with REXML" do
140
+ it_behaves_like "opal XML adapter round-trip", :rexml
141
+
142
+ it "selects REXML when explicitly configured" do
143
+ Lutaml::Model::Config.xml_adapter_type = :rexml
144
+ expect(Lutaml::Model::Config.xml_adapter_type).to eq(:rexml)
145
+ end
144
146
  end
145
147
  end
@@ -100,8 +100,8 @@ RSpec.describe "XML Prefix Control" do
100
100
  expect(xml).to include('<custom:Properties xmlns:custom="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">')
101
101
  expect(xml).to include("<custom:Template>Normal.dotm</custom:Template>")
102
102
  # Should not have redundant declarations
103
- expect(xml).not_to match(/xmlns="/)
104
- expect(xml).not_to match(/xmlns:app=/)
103
+ expect(xml).not_to include('xmlns="')
104
+ expect(xml).not_to include("xmlns:app=")
105
105
  end
106
106
  end
107
107
 
@@ -208,8 +208,8 @@ RSpec.describe "XML Prefix Control" do
208
208
  expect(xml).to include('<custom:Properties xmlns:custom="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">')
209
209
  expect(xml).to include("<custom:Template>Normal.dotm</custom:Template>")
210
210
  # Should not have redundant declarations
211
- expect(xml).not_to match(/xmlns="/)
212
- expect(xml).not_to match(/xmlns:app=/)
211
+ expect(xml).not_to include('xmlns="')
212
+ expect(xml).not_to include("xmlns:app=")
213
213
  end
214
214
  end
215
215
 
@@ -41,8 +41,9 @@ RSpec.describe Lutaml::Model::Schema::XmlCompiler do
41
41
  it "keeps built-in W3C XML attribute types when processing xml.xsd" do
42
42
  described_class.as_models(xml_namespace_schema)
43
43
 
44
- expect(described_class.attributes["id"].type)
45
- .to eq("Lutaml::Xml::W3c::XmlIdType")
44
+ type_ref = described_class.builder.attributes["id"].type
45
+ expect(type_ref.kind).to eq(:w3c)
46
+ expect(type_ref.value).to eq("Lutaml::Xml::W3c::XmlIdType")
46
47
  end
47
48
  end
48
49
 
@@ -227,8 +228,7 @@ RSpec.describe Lutaml::Model::Schema::XmlCompiler do
227
228
  end
228
229
 
229
230
  let(:expected_classes) do
230
- types = described_class::SimpleType::SUPPORTED_DATA_TYPES
231
- classes = types.filter_map do |name, value|
231
+ classes = described_class::SupportedDataTypes::TABLE.filter_map do |name, value|
232
232
  name.to_s unless value[:skippable]
233
233
  end
234
234
  classes << "User"
@@ -751,4 +751,21 @@ RSpec.describe Lutaml::Model::Schema::XmlCompiler do
751
751
  end
752
752
  end
753
753
  end
754
+
755
+ describe "SupportedDataTypes.skippable?" do
756
+ # Attribute TypeRefs store the snake_case rendering, simple_content
757
+ # keeps the XSD spelling; skippable? must accept both for dateTime,
758
+ # the one skippable name that changes under snake_case.
759
+ it "matches both the XSD and snake_case spelling of skippable types" do
760
+ sdt = described_class::SupportedDataTypes
761
+ expect(sdt.skippable?("dateTime")).to be true
762
+ expect(sdt.skippable?("date_time")).to be true
763
+ end
764
+
765
+ it "still rejects non-skippable types in both spellings" do
766
+ sdt = described_class::SupportedDataTypes
767
+ expect(sdt.skippable?("nonNegativeInteger")).to be false
768
+ expect(sdt.skippable?("non_negative_integer")).to be false
769
+ end
770
+ end
754
771
  end
@@ -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