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
@@ -87,6 +87,50 @@ module RawElementSpec
87
87
  end
88
88
  end
89
89
 
90
+ # mixed_content parent with a collection raw: :element mapping.
91
+ # Round-trip dispatches through OrderedApplier#apply_element_rule_single
92
+ # (see issue #727 — previously double-wrapped each fragment).
93
+ class MixedContentRawCollectionContainer < Lutaml::Model::Serializable
94
+ attribute :name, :string
95
+ attribute :fragments, :string, collection: true
96
+
97
+ xml do
98
+ element "container"
99
+ mixed_content
100
+ map_element "name", to: :name
101
+ map_element "fragment", to: :fragments, raw: :element
102
+ end
103
+ end
104
+
105
+ # mixed_content parent with a single (non-collection) raw: :element mapping.
106
+ # Dispatches through OrderedApplier -> :apply_rule -> RuleApplier#apply_rule,
107
+ # which already had the short-circuit. Kept as a regression guard.
108
+ class MixedContentRawSingleContainer < Lutaml::Model::Serializable
109
+ attribute :name, :string
110
+ attribute :embedded, :string
111
+
112
+ xml do
113
+ element "container"
114
+ mixed_content
115
+ map_element "name", to: :name
116
+ map_element "foreign", to: :embedded, raw: :element
117
+ end
118
+ end
119
+
120
+ # ordered (no mixed_content) parent with a collection raw: :element mapping.
121
+ # Same dispatch path as mixed_content; same bug class.
122
+ class OrderedRawCollectionContainer < Lutaml::Model::Serializable
123
+ attribute :name, :string
124
+ attribute :fragments, :string, collection: true
125
+
126
+ xml do
127
+ element "container"
128
+ ordered
129
+ map_element "name", to: :name
130
+ map_element "fragment", to: :fragments, raw: :element
131
+ end
132
+ end
133
+
90
134
  RSpec.describe "map_element raw: :element" do
91
135
  describe "deserialization" do
92
136
  describe "foreign namespace element capture" do
@@ -378,6 +422,87 @@ module RawElementSpec
378
422
  end
379
423
  end
380
424
 
425
+ # Regression for issue #727: OrderedApplier#apply_element_rule_single
426
+ # was missing the raw: :element short-circuit, so each captured fragment
427
+ # string (which already contains its own <fragment> wrapper) got wrapped
428
+ # again, producing <fragment><fragment>...</fragment></fragment>.
429
+ describe "round-trip with mixed_content + raw: :element collection" do
430
+ let(:mixed_collection_xml) do
431
+ <<~XML
432
+ <container>
433
+ before
434
+ <name>multi</name>
435
+ <fragment id="1">first</fragment>
436
+ <fragment id="2"><nested>second</nested></fragment>
437
+ after
438
+ </container>
439
+ XML
440
+ end
441
+
442
+ it "does not double-wrap fragments on round-trip" do
443
+ doc = MixedContentRawCollectionContainer.from_xml(mixed_collection_xml)
444
+ output = doc.to_xml
445
+
446
+ expect(output).not_to include("<fragment><fragment")
447
+ expect(output).to include('id="1"')
448
+ expect(output).to include("first")
449
+ expect(output).to include('id="2"')
450
+ expect(output).to include("<nested>second</nested>")
451
+ end
452
+
453
+ it "is stable across repeated round-trips" do
454
+ doc = MixedContentRawCollectionContainer.from_xml(mixed_collection_xml)
455
+ once = MixedContentRawCollectionContainer.from_xml(doc.to_xml)
456
+ twice = MixedContentRawCollectionContainer.from_xml(once.to_xml)
457
+
458
+ expect(twice.fragments.length).to eq(2)
459
+ expect(twice.fragments[0]).to include("first")
460
+ expect(twice.fragments[1]).to include("<nested>second</nested>")
461
+ end
462
+ end
463
+
464
+ # Regression guard: the non-collection path under mixed_content dispatches
465
+ # through :apply_rule -> RuleApplier#apply_rule, which already had the
466
+ # short-circuit. Lock in the working behavior.
467
+ describe "round-trip with mixed_content + raw: :element single" do
468
+ let(:mixed_single_xml) do
469
+ '<container>before<name>x</name><foreign attr="v"><child>c</child></foreign>after</container>'
470
+ end
471
+
472
+ it "emits the foreign element verbatim without wrapping" do
473
+ doc = MixedContentRawSingleContainer.from_xml(mixed_single_xml)
474
+ output = doc.to_xml
475
+
476
+ expect(output).not_to include("<foreign><foreign")
477
+ expect(output).to include('attr="v"')
478
+ expect(output).to include("<child>c</child>")
479
+ end
480
+ end
481
+
482
+ # Same dispatch path (OrderedApplier) as mixed_content; same bug class.
483
+ describe "round-trip with ordered + raw: :element collection" do
484
+ let(:ordered_collection_xml) do
485
+ <<~XML
486
+ <container>
487
+ <name>multi</name>
488
+ <fragment id="1">first</fragment>
489
+ <fragment id="2">second</fragment>
490
+ </container>
491
+ XML
492
+ end
493
+
494
+ it "does not double-wrap fragments on round-trip" do
495
+ doc = OrderedRawCollectionContainer.from_xml(ordered_collection_xml)
496
+ output = doc.to_xml
497
+
498
+ expect(output).not_to include("<fragment><fragment")
499
+ expect(output).to include('id="1"')
500
+ expect(output).to include("first")
501
+ expect(output).to include('id="2"')
502
+ expect(output).to include("second")
503
+ end
504
+ end
505
+
381
506
  describe "nil and empty values" do
382
507
  it "omits raw element when value is nil" do
383
508
  model = SvgContainer.new(svg_data: nil)
@@ -9,19 +9,10 @@ RSpec.describe Lutaml::Model::RuleValueExtractor do
9
9
  let(:rule) { instance_double(Lutaml::Model::Jsonl::MappingRule) }
10
10
  let(:doc) { { "name" => "Test", "value" => 123 } }
11
11
  let(:format) { :json }
12
- let(:attr) { instance_double(Lutaml::Model::Attribute) }
13
- let(:register) { instance_double(Lutaml::Model::Register) }
12
+ let(:attr) { Lutaml::Model::Attribute.new("name", :string) }
13
+ let(:register) { Lutaml::Model::Config.default_register }
14
14
  let(:options) { {} }
15
15
 
16
- def mock_resolver(default_set_value, default_value_data = nil)
17
- resolver_double = instance_double(Lutaml::Model::Services::DefaultValueResolver,
18
- default_set?: default_set_value,
19
- default_value: default_value_data)
20
- allow(Lutaml::Model::Services::DefaultValueResolver).to receive(:new)
21
- .with(attr, register, instance)
22
- .and_return(resolver_double)
23
- end
24
-
25
16
  describe "#call" do
26
17
  context "when rule has single mapping" do
27
18
  before do
@@ -82,7 +73,7 @@ RSpec.describe Lutaml::Model::RuleValueExtractor do
82
73
  end
83
74
  end
84
75
 
85
- context "when value is not found" do
76
+ context "when value is not found and attribute has no default" do
86
77
  before do
87
78
  allow(rule).to receive_messages(
88
79
  multiple_mappings?: false,
@@ -90,7 +81,6 @@ RSpec.describe Lutaml::Model::RuleValueExtractor do
90
81
  root_mapping?: false,
91
82
  raw_mapping?: false,
92
83
  )
93
- mock_resolver(false)
94
84
  end
95
85
 
96
86
  it "returns uninitialized value" do
@@ -99,6 +89,10 @@ RSpec.describe Lutaml::Model::RuleValueExtractor do
99
89
  end
100
90
 
101
91
  context "when attribute has default value" do
92
+ let(:attr) do
93
+ Lutaml::Model::Attribute.new("name", :string, default: "default_value")
94
+ end
95
+
102
96
  before do
103
97
  allow(rule).to receive_messages(
104
98
  multiple_mappings?: false,
@@ -106,7 +100,6 @@ RSpec.describe Lutaml::Model::RuleValueExtractor do
106
100
  root_mapping?: false,
107
101
  raw_mapping?: false,
108
102
  )
109
- mock_resolver(true, "default_value")
110
103
  end
111
104
 
112
105
  it "returns default value" do
@@ -33,4 +33,29 @@ RSpec.describe Lutaml::Model::RuntimeCompatibility do
33
33
  .to eq(error_class)
34
34
  end
35
35
  end
36
+
37
+ # lib/lutaml/model.rb and lib/lutaml/xml.rb re-evaluate their top-level
38
+ # prepend calls when Opal's eager loader processes them more than once.
39
+ # Opal's Module#prepend raises "Prepending a module multiple times is
40
+ # not supported" in that case; runtime_compatibility.rb aligns Opal
41
+ # with MRI's idempotent behavior. MRI has been idempotent since 2.0,
42
+ # so the spec applies to both runtimes.
43
+ describe "Module#prepend idempotency" do
44
+ it "is a no-op when the module is already in the ancestor chain" do
45
+ mod = Module.new
46
+ klass = Class.new
47
+ klass.prepend(mod)
48
+
49
+ expect { klass.prepend(mod) }.not_to raise_error
50
+ expect(klass.ancestors.count(mod)).to eq(1)
51
+ end
52
+
53
+ it "still prepends when the module is not yet present" do
54
+ mod = Module.new
55
+ klass = Class.new
56
+
57
+ expect { klass.prepend(mod) }.not_to raise_error
58
+ expect(klass.ancestors).to include(mod)
59
+ end
60
+ end
36
61
  end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/schema/definitions"
5
+ require "lutaml/model/schema/renderers/model"
6
+
7
+ RSpec.describe Lutaml::Model::Schema::Renderers::Model do
8
+ let(:defs) { Lutaml::Model::Schema::Definitions }
9
+
10
+ it "renders a rooted element with one element attribute" do
11
+ spec = defs::Model.new(
12
+ class_name: "Person",
13
+ xml_root: defs::XmlRoot.new(kind: :element, name: "Person"),
14
+ members: [
15
+ defs::Attribute.new(
16
+ name: "name",
17
+ type: defs::TypeRef.new(kind: :symbol, value: "string"),
18
+ xml_name: "name",
19
+ kind: :element,
20
+ ),
21
+ ],
22
+ )
23
+
24
+ output = described_class.render(spec)
25
+
26
+ expect(output).to include("class Person < Lutaml::Model::Serializable")
27
+ expect(output).to include("attribute :name, :string")
28
+ expect(output).to include("xml do")
29
+ expect(output).to include('element "Person"')
30
+ expect(output).to include('map_element "name", to: :name')
31
+ expect(output).to include("def self.register")
32
+ expect(output).to include("Person.register_class_with_id")
33
+ end
34
+
35
+ it "renders a fragment (no element / type_name directive)" do
36
+ spec = defs::Model.new(
37
+ class_name: "Inner",
38
+ xml_root: defs::XmlRoot.new(kind: :fragment, name: nil),
39
+ )
40
+
41
+ output = described_class.render(spec)
42
+
43
+ expect(output).not_to include("element ")
44
+ expect(output).not_to include("type_name ")
45
+ end
46
+
47
+ it "renders an attribute mapping with map_attribute" do
48
+ spec = defs::Model.new(
49
+ class_name: "Item",
50
+ xml_root: defs::XmlRoot.new(kind: :element, name: "Item"),
51
+ members: [
52
+ defs::Attribute.new(
53
+ name: "id",
54
+ type: defs::TypeRef.new(kind: :symbol, value: "string"),
55
+ xml_name: "id",
56
+ kind: :attribute,
57
+ ),
58
+ ],
59
+ )
60
+
61
+ output = described_class.render(spec)
62
+
63
+ expect(output).to include('map_attribute "id", to: :id')
64
+ end
65
+
66
+ it "renders a class reference type with no leading colon" do
67
+ spec = defs::Model.new(
68
+ class_name: "Owner",
69
+ xml_root: defs::XmlRoot.new(kind: :element, name: "Owner"),
70
+ members: [
71
+ defs::Attribute.new(
72
+ name: "address",
73
+ type: defs::TypeRef.new(kind: :class_ref, value: "Address"),
74
+ xml_name: "address",
75
+ kind: :element,
76
+ ),
77
+ ],
78
+ )
79
+
80
+ output = described_class.render(spec)
81
+
82
+ expect(output).to include("attribute :address, Address")
83
+ end
84
+
85
+ it "renders a w3c type with the ::-prefixed reference" do
86
+ spec = defs::Model.new(
87
+ class_name: "Para",
88
+ xml_root: defs::XmlRoot.new(kind: :element, name: "Para"),
89
+ members: [
90
+ defs::Attribute.new(
91
+ name: "lang",
92
+ type: defs::TypeRef.new(kind: :w3c, value: "Lutaml::Xml::W3c::Lang"),
93
+ xml_name: "lang",
94
+ kind: :attribute,
95
+ ),
96
+ ],
97
+ )
98
+
99
+ output = described_class.render(spec)
100
+
101
+ expect(output).to include("attribute :lang, ::Lutaml::Xml::W3c::Lang")
102
+ end
103
+
104
+ it "skips register methods inside a module_namespace" do
105
+ spec = defs::Model.new(
106
+ class_name: "Inside",
107
+ xml_root: defs::XmlRoot.new(kind: :element, name: "Inside"),
108
+ )
109
+
110
+ output = described_class.render(spec, module_namespace: "Mod::Sub")
111
+
112
+ expect(output).to include("module Mod")
113
+ expect(output).to include("module Sub")
114
+ expect(output).not_to include("def self.register")
115
+ expect(output).not_to include("register_class_with_id")
116
+ end
117
+
118
+ it "renders a choice block with attributes inside" do
119
+ spec = defs::Model.new(
120
+ class_name: "Either",
121
+ xml_root: defs::XmlRoot.new(kind: :element, name: "Either"),
122
+ members: [
123
+ defs::Choice.new(
124
+ header: "choice",
125
+ alternatives: [
126
+ defs::Attribute.new(
127
+ name: "alpha",
128
+ type: defs::TypeRef.new(kind: :symbol, value: "string"),
129
+ xml_name: "alpha",
130
+ kind: :element,
131
+ ),
132
+ defs::Attribute.new(
133
+ name: "beta",
134
+ type: defs::TypeRef.new(kind: :symbol, value: "string"),
135
+ xml_name: "beta",
136
+ kind: :element,
137
+ ),
138
+ ],
139
+ ),
140
+ ],
141
+ )
142
+
143
+ output = described_class.render(spec)
144
+
145
+ expect(output).to include("choice do")
146
+ expect(output).to include("attribute :alpha, :string")
147
+ expect(output).to include("attribute :beta, :string")
148
+ end
149
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/schema/definitions"
5
+ require "lutaml/model/schema/renderers/namespace"
6
+
7
+ RSpec.describe Lutaml::Model::Schema::Renderers::Namespace do
8
+ let(:defs) { Lutaml::Model::Schema::Definitions }
9
+
10
+ it "renders a namespace class with uri and prefix_default" do
11
+ spec = defs::Namespace.new(
12
+ class_name: "ExampleNs",
13
+ uri: "http://example.com/ns",
14
+ prefix_default: "ex",
15
+ )
16
+
17
+ output = described_class.render(spec)
18
+
19
+ expect(output).to include("class ExampleNs < Lutaml::Xml::W3c::XmlNamespace")
20
+ expect(output).to include(%(uri "http://example.com/ns"))
21
+ expect(output).to include(%(prefix_default "ex"))
22
+ end
23
+
24
+ it "wraps in module when module_namespace is supplied" do
25
+ spec = defs::Namespace.new(
26
+ class_name: "Inner",
27
+ uri: "http://example.com/inner",
28
+ prefix_default: nil,
29
+ )
30
+
31
+ output = described_class.render(spec, module_namespace: "Mod::Sub")
32
+
33
+ expect(output).to include("module Mod")
34
+ expect(output).to include("module Sub")
35
+ end
36
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/schema/definitions"
5
+ require "lutaml/model/schema/renderers/restricted_type"
6
+
7
+ RSpec.describe Lutaml::Model::Schema::Renderers::RestrictedType do
8
+ let(:defs) { Lutaml::Model::Schema::Definitions }
9
+
10
+ it "renders min/max facets in the cast body" do
11
+ spec = defs::RestrictedType.new(
12
+ class_name: "PositiveInt",
13
+ parent_class: "Lutaml::Model::Type::Integer",
14
+ facets: defs::Facet.new(min_inclusive: 1, max_inclusive: 100),
15
+ )
16
+
17
+ output = described_class.render(spec)
18
+
19
+ expect(output).to include("class PositiveInt < Lutaml::Model::Type::Integer")
20
+ expect(output).to include("def self.cast(value, options = {})")
21
+ expect(output).to include("options[:min] = 1")
22
+ expect(output).to include("options[:max] = 100")
23
+ expect(output).to include("value = super(value, options)")
24
+ end
25
+
26
+ it "renders enumerations facet" do
27
+ spec = defs::RestrictedType.new(
28
+ class_name: "Color",
29
+ parent_class: "Lutaml::Model::Type::String",
30
+ facets: defs::Facet.new(enumerations: %w[red green blue]),
31
+ )
32
+
33
+ output = described_class.render(spec)
34
+
35
+ expect(output).to include('options[:values] = [super("red"), super("green"), super("blue")]')
36
+ expect(output).not_to include("options[:min]")
37
+ end
38
+
39
+ it "renders pattern facet as a regexp literal" do
40
+ spec = defs::RestrictedType.new(
41
+ class_name: "Slug",
42
+ parent_class: "Lutaml::Model::Type::String",
43
+ facets: defs::Facet.new(pattern: "[a-z0-9-]+"),
44
+ )
45
+
46
+ output = described_class.render(spec)
47
+
48
+ expect(output).to include("options[:pattern] = %r{[a-z0-9-]+}")
49
+ end
50
+
51
+ it "renders transform facet" do
52
+ spec = defs::RestrictedType.new(
53
+ class_name: "Upper",
54
+ parent_class: "Lutaml::Model::Type::String",
55
+ facets: defs::Facet.new,
56
+ transform_facet: defs::TransformFacet.new(expression: "value.upcase"),
57
+ )
58
+
59
+ output = described_class.render(spec)
60
+
61
+ expect(output).to include("value = value.upcase")
62
+ end
63
+
64
+ it "uses lazy register memoisation" do
65
+ spec = defs::RestrictedType.new(
66
+ class_name: "Tiny",
67
+ parent_class: "Lutaml::Model::Type::Integer",
68
+ facets: defs::Facet.new(max_inclusive: 5),
69
+ )
70
+
71
+ output = described_class.render(spec)
72
+
73
+ expect(output).to include("@register ||= Lutaml::Model::Config.default_register")
74
+ end
75
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/schema/definitions"
5
+ require "lutaml/model/schema/renderers/union"
6
+
7
+ RSpec.describe Lutaml::Model::Schema::Renderers::Union do
8
+ let(:defs) { Lutaml::Model::Schema::Definitions }
9
+
10
+ it "renders the :class_refs strategy as an each/begin/rescue loop" do
11
+ spec = defs::UnionType.new(
12
+ class_name: "NumberOrString",
13
+ members: [
14
+ defs::TypeRef.new(kind: :class_ref, value: "Integer"),
15
+ defs::TypeRef.new(kind: :class_ref, value: "String"),
16
+ ],
17
+ cast_strategy: :class_refs,
18
+ )
19
+
20
+ output = described_class.render(spec)
21
+
22
+ expect(output).to include("class NumberOrString < Lutaml::Model::Type::Value")
23
+ expect(output).to include("[Integer, String].each do |t|")
24
+ expect(output).to include("casted = t.cast(value, options)")
25
+ expect(output).to include("return casted unless casted.nil?")
26
+ expect(output).to include("rescue StandardError")
27
+ end
28
+
29
+ it "renders the :resolve_type strategy as a ||-chained resolve_type call" do
30
+ spec = defs::UnionType.new(
31
+ class_name: "IntegerOrToken",
32
+ members: [
33
+ defs::TypeRef.new(kind: :symbol, value: "integer"),
34
+ defs::TypeRef.new(kind: :symbol, value: "token"),
35
+ ],
36
+ cast_strategy: :resolve_type,
37
+ )
38
+
39
+ output = described_class.render(spec)
40
+
41
+ expect(output).to include("Lutaml::Model::GlobalContext.resolve_type(:integer, @register).cast(value, options)")
42
+ expect(output).to include("Lutaml::Model::GlobalContext.resolve_type(:token, @register).cast(value, options)")
43
+ expect(output).to include(" ||\n")
44
+ end
45
+
46
+ it "emits required_files lines above the module wrap" do
47
+ spec = defs::UnionType.new(
48
+ class_name: "AnyOf",
49
+ members: [defs::TypeRef.new(kind: :class_ref, value: "Integer")],
50
+ cast_strategy: :class_refs,
51
+ required_files: ['require_relative "integer"'],
52
+ )
53
+
54
+ output = described_class.render(spec)
55
+
56
+ expect(output).to include('require_relative "integer"')
57
+ end
58
+ end
@@ -0,0 +1,64 @@
1
+ require "spec_helper"
2
+
3
+ # Regression: Transform#apply_value_map must handle the nil-attr case used
4
+ # by custom-method-only key-value rules. Pre-consolidation behavior:
5
+ # the default { omitted: :nil, nil: :nil, empty: :empty } map converted
6
+ # UninitializedClass.instance to nil so the downstream Utils.present? guard
7
+ # in KeyValue::Transform#process_mapping_rule would skip the custom method
8
+ # for absent input keys.
9
+ RSpec.describe Lutaml::Model::Transform, "#apply_value_map (nil attr)" do
10
+ let(:transform_subclass) do
11
+ Class.new(described_class) do
12
+ # Expose the protected delegator for direct testing.
13
+ public :apply_value_map
14
+ end
15
+ end
16
+
17
+ let(:context) { Class.new(Lutaml::Model::Serializable) }
18
+ let(:register) { Lutaml::Model::Config.default_register }
19
+ let(:transform) { transform_subclass.new(context, register) }
20
+
21
+ let(:uninit) { Lutaml::Model::UninitializedClass.instance }
22
+ let(:default_vmap) { { omitted: :nil, nil: :nil, empty: :empty } }
23
+
24
+ describe "default mapping with nil attr" do
25
+ it "converts UninitializedClass to nil via the :omitted => :nil mapping" do
26
+ # Before the fix: returned uninit unchanged, causing Utils.present? to
27
+ # be true and the custom method to run for absent fields.
28
+ expect(transform.apply_value_map(uninit, default_vmap, nil)).to be_nil
29
+ end
30
+
31
+ it "converts nil to nil via the :nil => :nil mapping" do
32
+ expect(transform.apply_value_map(nil, default_vmap, nil)).to be_nil
33
+ end
34
+
35
+ it "passes initialized values through unchanged" do
36
+ expect(transform.apply_value_map("hello", default_vmap, nil)).to eq("hello")
37
+ end
38
+ end
39
+
40
+ describe "explicit :omitted => :omitted with nil attr" do
41
+ let(:vmap) { { omitted: :omitted, nil: :nil, empty: :empty } }
42
+
43
+ it "preserves UninitializedClass for explicit omitted: :omitted" do
44
+ expect(transform.apply_value_map(uninit, vmap, nil)).to eq(uninit)
45
+ end
46
+ end
47
+
48
+ # Behavior-preservation: the pre-consolidation attribute-less path mapped the
49
+ # :omitted option to UninitializedClass.instance (present? => true, so the
50
+ # custom method runs), and resolved :nil/:empty consistently for empty inputs.
51
+ describe "non-:nil options with nil attr" do
52
+ it "maps a nil value to uninitialized via nil: :omitted" do
53
+ expect(transform.apply_value_map(nil, { nil: :omitted }, nil)).to eq(uninit)
54
+ end
55
+
56
+ it "maps an empty value to uninitialized via empty: :omitted" do
57
+ expect(transform.apply_value_map("", { empty: :omitted }, nil)).to eq(uninit)
58
+ end
59
+
60
+ it "maps an empty value to nil via empty: :nil" do
61
+ expect(transform.apply_value_map("", { empty: :nil }, nil)).to be_nil
62
+ end
63
+ end
64
+ end
@@ -23,7 +23,7 @@ RSpec.describe Lutaml::Model::UninitializedClass do
23
23
 
24
24
  describe "#match?" do
25
25
  it "returns false for any argument" do
26
- expect(uninitialized).not_to match /pattern/
26
+ expect(uninitialized).not_to include("pattern")
27
27
  expect(uninitialized).not_to match "string"
28
28
  expect(uninitialized).not_to match nil
29
29
  end