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
@@ -509,6 +509,148 @@ Parent.new(child: Child.new(value: "test")).to_xml(prefix: true)
509
509
 
510
510
  All three elements (`parent`, `child`, `value`) use the prefix because all are qualified via `element_form_default: :qualified`.
511
511
 
512
+ === Form Override on Serializable Children
513
+
514
+ The `form:` option works on attributes of any type — including attributes whose value type is another `Serializable` model, not only simple types like `:string`. This matters when the parent's namespace declares `element_form_default :unqualified` (the W3C default) and you need a specific nested model element to be qualified anyway.
515
+
516
+ .Worked example: form: :qualified on a Serializable child
517
+ [example]
518
+ ====
519
+ [source,ruby]
520
+ ----
521
+ NS = Class.new(Lutaml::Model::XmlNamespace) do
522
+ uri "https://example.com/ns"
523
+ prefix_default "ex"
524
+ element_form_default :unqualified # W3C default for local elements
525
+ end
526
+
527
+ class Child < Lutaml::Model::Serializable
528
+ attribute :label, :string
529
+ xml do
530
+ element "child"
531
+ namespace NS
532
+ map_element "label", to: :label
533
+ end
534
+ end
535
+
536
+ class Parent < Lutaml::Model::Serializable
537
+ attribute :child, Child
538
+ xml do
539
+ element "item"
540
+ namespace NS
541
+ map_element "child", to: :child, form: :qualified # Force prefix
542
+ end
543
+ end
544
+
545
+ Parent.new(child: Child.new(label: "x")).to_xml
546
+ ----
547
+
548
+ *Output*:
549
+ [source,xml]
550
+ ----
551
+ <item xmlns="https://example.com/ns" xmlns:ex="https://example.com/ns">
552
+ <ex:child>
553
+ <label>x</label>
554
+ </ex:child>
555
+ </item>
556
+ ----
557
+
558
+ The `ex:` prefix on `<child>` is forced by `form: :qualified`, overriding the parent's `:unqualified` schema default. The inner `<label>` remains unprefixed because the `Child` model's own mapping rule for `label` has no `form:` override — see the next section.
559
+ ====
560
+
561
+ === Form Scope: Per-Rule, Not Transitive
562
+
563
+ `form:` is a per-mapping-rule override. It does **not** propagate transitively to grandchildren. Each level of the model tree applies its own rule against its own parent's `element_form_default`.
564
+
565
+ .Worked example: form on parent does not cascade to grandchild
566
+ [example]
567
+ ====
568
+ [source,ruby]
569
+ ----
570
+ NS = Class.new(Lutaml::Model::XmlNamespace) do
571
+ uri "https://example.com/ns"
572
+ prefix_default "ex"
573
+ element_form_default :unqualified
574
+ end
575
+
576
+ class Grandchild < Lutaml::Model::Serializable
577
+ attribute :value, :string
578
+ xml do
579
+ element "grandchild"
580
+ namespace NS
581
+ map_element "value", to: :value
582
+ end
583
+ end
584
+
585
+ class Child < Lutaml::Model::Serializable
586
+ attribute :inner, Grandchild
587
+ xml do
588
+ element "child"
589
+ namespace NS
590
+ map_element "grandchild", to: :inner # NO form: override
591
+ end
592
+ end
593
+
594
+ class Parent < Lutaml::Model::Serializable
595
+ attribute :child, Child
596
+ xml do
597
+ element "item"
598
+ namespace NS
599
+ map_element "child", to: :child, form: :qualified # Parent's rule only
600
+ end
601
+ end
602
+
603
+ Parent.new(child: Child.new(inner: Grandchild.new(value: "x"))).to_xml
604
+ ----
605
+
606
+ *Output*:
607
+ [source,xml]
608
+ ----
609
+ <item xmlns="https://example.com/ns" xmlns:ex="https://example.com/ns">
610
+ <ex:child>
611
+ <grandchild>
612
+ <value>x</value>
613
+ </grandchild>
614
+ </ex:child>
615
+ </item>
616
+ ----
617
+
618
+ * `<ex:child>` is qualified because the *Parent* rule has `form: :qualified`.
619
+ * `<grandchild>` is unprefixed because the *Child* rule has no `form:` and the Child's namespace is `:unqualified`.
620
+ * The Parent's `form: :qualified` does **not** cascade.
621
+ ====
622
+
623
+ To qualify every level, either set `form: :qualified` on each mapping rule that needs it, or set `element_form_default :qualified` on the namespace so inheritance handles it.
624
+
625
+ == Qualification Precedence
626
+
627
+ When serializing an element, the namespace is resolved by checking the following sources in priority order. The first match wins.
628
+
629
+ [cols="1,4", options="header"]
630
+ |===
631
+ |Priority |Source
632
+
633
+ |1 (highest)
634
+ |Type-level namespace: the attribute's value type (a `Type::Value` subclass) declares `xml_namespace`. Wins over everything else.
635
+
636
+ |2
637
+ |Rule-level namespace: the mapping rule has an explicit `namespace:` option. *However*, if the parent's `element_form_default` is `:unqualified` AND the rule's namespace matches the parent's, the namespace is overridden to blank — W3C unqualified semantics for local elements.
638
+
639
+ |3
640
+ |`form: :unqualified` on the rule: force the element into no namespace.
641
+
642
+ |4
643
+ |Parent's `element_form_default :qualified` inheritance: the child inherits the parent's namespace — but only if the child model itself declares a namespace (W3C: `elementFormDefault` applies to locally-declared elements only).
644
+
645
+ |5
646
+ |`form: :qualified` on the rule: inherit the parent's namespace.
647
+
648
+ |6 (lowest)
649
+ |No namespace.
650
+ |===
651
+
652
+ This ladder is implemented in `Lutaml::Xml::Transformation::ElementBuilder#determine_element_namespace`. The `ElementFormOptionRule` decision rule reads the propagated `form` value during the planning phase and forces prefix (`:qualified`) or default (`:unqualified`) format accordingly.
653
+
512
654
  == Type Namespaces
513
655
 
514
656
  Value types can define their own namespaces:
@@ -72,6 +72,71 @@ The `required: true` option validates that an attribute is present.
72
72
  attribute :name, :string, required: true
73
73
  ----
74
74
 
75
+ === XML schema validation
76
+
77
+ The `validate_xml_with` class macro validates the model's generated XML
78
+ against one or more XML Schema (XSD) files during `validate` / `validate!`.
79
+
80
+ [source,ruby]
81
+ ----
82
+ class Person < Lutaml::Model::Serializable
83
+ attribute :name, :string
84
+
85
+ xml do
86
+ root "person"
87
+ map_element "name", to: :name
88
+ end
89
+
90
+ validate_xml_with "schemas/person.xsd"
91
+ end
92
+
93
+ person = Person.new(name: "Alice")
94
+ person.validate # => collects Lutaml::Xml::Error::SchemaValidationError
95
+ # instances when the generated XML does not conform
96
+ person.validate! # => raises Lutaml::Model::ValidationError on nonconformance
97
+ ----
98
+
99
+ The macro accepts one or more schema paths, and repeated calls append to the
100
+ configured list. Subclasses inherit the parent's schema paths parent-first;
101
+ paths added by a subclass do not affect the parent.
102
+
103
+ [source,ruby]
104
+ ----
105
+ validate_xml_with "schemas/base.xsd", "schemas/extensions.xsd"
106
+ ----
107
+
108
+ Raw XML strings can be checked against the same schemas with the explicit
109
+ class-level helpers, for example to reject nonconforming input before
110
+ `from_xml`:
111
+
112
+ [source,ruby]
113
+ ----
114
+ Person.validate_xml(xml_string) # => array of schema validation errors
115
+ Person.validate_xml!(xml_string) # => raises Lutaml::Model::ValidationError
116
+ ----
117
+
118
+ NOTE: `to_xml` and `from_xml` never validate implicitly. Call `validate!`
119
+ before serializing, or `validate_xml!` on incoming XML, when enforcement is
120
+ needed — the same explicit contract as every other validation type.
121
+
122
+ XSD validation uses Nokogiri, which is required lazily on first validation.
123
+ The schemas must be local files declared explicitly; `xsi:schemaLocation`
124
+ in documents is not read and remote schemas are not fetched. Relative paths
125
+ resolve against the file that declares `validate_xml_with`, so a model works
126
+ regardless of the process working directory.
127
+
128
+ Misconfiguration raises instead of being collected as validation errors:
129
+
130
+ * a missing schema file raises `Errno::ENOENT`;
131
+ * a malformed XSD raises `Nokogiri::XML::SyntaxError`;
132
+ * a malformed XML string passed to `validate_xml` / `validate_xml!` raises
133
+ `Nokogiri::XML::SyntaxError` — parsing is strict, so malformed input is
134
+ never silently repaired and reported as conforming;
135
+ * a missing nokogiri gem raises `Lutaml::Xml::Error::XmlConfigurationError`;
136
+ * a class using the macro without an XML root mapping raises
137
+ `Lutaml::Model::TypeOnlyMappingError` from `validate`, since validation
138
+ must serialize the model to check it.
139
+
75
140
 
76
141
  == Validation examples
77
142
 
@@ -9,6 +9,8 @@ This guide explains how XML namespaces work, particularly the distinction betwee
9
9
  - **Qualified**: Element/attribute needs a namespace declared in the XML
10
10
  - **Unqualified**: Element/attribute does not need a namespace declared (uses parent's namespace)
11
11
 
12
+ > **Authoritative reference:** For the `element_form_default` / `form:` system, the full qualification precedence ladder, and `form:` behavior on nested `Serializable` children, see [docs/_guides/xml-namespace-qualification.adoc](../_guides/xml-namespace-qualification.adoc). This tutorial is a conceptual introduction.
13
+
12
14
  ## Default Namespace Inheritance
13
15
 
14
16
  **Critical Rule:** Default namespace inheritance ONLY applies to elements, NOT attributes.
@@ -441,6 +441,8 @@ end
441
441
 
442
442
  NOTE: The `<comment>` element uses `xmlns=""` to explicitly declare blank namespace (form: :unqualified override).
443
443
 
444
+ TIP: The `form:` option also works on attributes whose value type is a nested `Serializable` model, not just simple types like `:string`. It is a per-rule override and does not propagate transitively to grandchildren. For the full qualification precedence ladder and worked nested-model examples, see link:../_guides/xml-namespace-qualification/#form-override-on-serializable-children[XML Namespace Qualification and Prefix Control].
445
+
444
446
  == Section 3: Type Namespaces
445
447
 
446
448
  === General
@@ -6,6 +6,8 @@
6
6
 
7
7
  This guide provides comprehensive information about XML namespace management in Lutaml::Model, including the `namespace_scope` directive, W3C compliance, and best practices for multi-namespace documents.
8
8
 
9
+ NOTE: This guide focuses on namespace *declaration* and *scope*. For the `element_form_default` / `form:` qualification system — including the precedence ladder and `form:` behavior on nested `Serializable` children — see the companion guide: link:./_guides/xml-namespace-qualification[XML Namespace Qualification and Prefix Control].
10
+
9
11
  == Understanding XML Namespaces
10
12
 
11
13
  XML namespaces provide a method to avoid element name conflicts by qualifying names used in XML documents through a URI reference.
@@ -13,6 +13,12 @@ declared in the XML, "unqualified" means the ELEMENT/ATTRIBUTE does not need a
13
13
  namespace declared in the XML (i.e. we look to its parent's namespace to find
14
14
  its namespace).
15
15
 
16
+ > **Authoritative reference:** For the full qualification precedence ladder,
17
+ > `form:` behavior on nested `Serializable` children, per-rule vs transitive
18
+ > scope, and worked examples, see
19
+ > [docs/_guides/xml-namespace-qualification.adoc](./_guides/xml-namespace-qualification.adoc).
20
+ > This document is a conceptual overview; the guide is the canonical reference.
21
+
16
22
  When there is no XML Schema present:
17
23
  * element "acts like" unqualified (because of default namespace inheritance, does not need prefix)
18
24
  * attribute "acts like" qualified (because of no default namespace inheritance, requires prefix)
@@ -0,0 +1,123 @@
1
+ # frozen_string: true
2
+
3
+ # One-shot generator for lib/compat/opal/lutaml_model_boot.rb.
4
+ #
5
+ # Walks lib/lutaml/**/*.rb and extracts every `autoload :Name, "path"`
6
+ # declaration (single-line or multi-line), resolving the standard
7
+ # `"#{__dir__}/..."` and `"#{File.dirname(__FILE__)}/..."` interpolation
8
+ # patterns relative to the declaring file's directory.
9
+ #
10
+ # Opal treats autoload as a no-op, so every autoload target must appear
11
+ # here as an explicit `require` for nested constants to resolve at runtime.
12
+ #
13
+ # Run after adding/removing any autoload in lib/lutaml/:
14
+ # ruby lib/compat/opal/generate_boot.rb
15
+
16
+ require "pathname"
17
+
18
+ REPO_ROOT = Pathname.new(File.expand_path("../../..", __dir__))
19
+ LIB = REPO_ROOT.join("lib")
20
+
21
+ # Patterns excluded under Opal because they pull in native-only deps
22
+ # (Nokogiri/Ox C extensions, Thor CLI, etc.). REXML is pure Ruby and
23
+ # ships in the bundled gem + moxml's lib/compat/opal/rexml/* shadows,
24
+ # so it is NOT excluded — it is a fully working second Opal adapter.
25
+ NATIVE_ONLY_PATTERNS = [
26
+ %r{(nokogiri|ox)_adapter\z},
27
+ %r{/schema/(relaxng|xsd)\b},
28
+ %r{/schema_builder/(nokogiri|oga)\b},
29
+ %r{/schema/builder/(nokogiri|oga)\b},
30
+ %r{\Alutaml/model/cli\z},
31
+ ].freeze
32
+
33
+ # Match `autoload :Name, "...anything..."` where the string argument may
34
+ # span multiple physical lines (whitespace after the comma).
35
+ DECL_RE = /autoload\s+:[A-Za-z0-9_]+\s*,\s*"([^"]+)"/
36
+
37
+ # Stitches multi-line autoload declarations into a single logical line so
38
+ # DECL_RE can match. We only merge when the line ends right after the
39
+ # comma that follows `autoload :Name`.
40
+ def join_multiline_autoloads(src)
41
+ src.gsub(/(autoload\s+:[A-Za-z0-9_]+\s*,)\s*\n\s*/) { "#{Regexp.last_match(1)} " }
42
+ end
43
+
44
+ # Resolve an autoload's raw string argument to a require path relative to
45
+ # LIB. Both common interpolation shapes resolve relative to the declaring
46
+ # file's directory, which is what MRI does for them.
47
+ def resolve_path(raw, declaring_file)
48
+ declaring_dir = Pathname.new(declaring_file).dirname
49
+
50
+ if raw.start_with?("\#{__dir__}/")
51
+ subdir = raw.sub("\#{__dir__}/", "").delete_suffix(".rb")
52
+ (declaring_dir + subdir).relative_path_from(LIB).to_s.delete_suffix(".rb")
53
+ elsif raw.start_with?("\#{File.dirname(__FILE__)}/")
54
+ subdir = raw.sub("\#{File.dirname(__FILE__)}/", "").delete_suffix(".rb")
55
+ (declaring_dir + subdir).relative_path_from(LIB).to_s.delete_suffix(".rb")
56
+ elsif raw.start_with?("lutaml/")
57
+ raw.delete_suffix(".rb")
58
+ else
59
+ raw.delete_suffix(".rb")
60
+ end
61
+ end
62
+
63
+ paths = []
64
+ Dir.glob("#{LIB}/lutaml/**/*.rb").each do |file|
65
+ src = File.read(file)
66
+ # Strip full-line and trailing comments so commented-out autoload
67
+ # examples (e.g. in type_registry.rb) don't end up in the boot file.
68
+ src = src.lines.reject { |line| line.lstrip.start_with?("#") }.join
69
+ join_multiline_autoloads(src).scan(DECL_RE).each do |(raw)|
70
+ paths << resolve_path(raw, file)
71
+ end
72
+ end
73
+
74
+ paths = paths.uniq.sort
75
+ native_only = paths.select { |p| NATIVE_ONLY_PATTERNS.any? { |re| p =~ re } }
76
+ opal_paths = paths - native_only
77
+
78
+ # Order matters: namespace-defining entry files must load before any of
79
+ # their nested files. lutaml/model.rb declares the Lutaml::Model module
80
+ # (referenced by every other file); lutaml/xml.rb declares Lutaml::Xml.
81
+ # After the entry files, load by depth-then-name so a parent file (e.g.
82
+ # lutaml/hash_format.rb) is required before its children (e.g.
83
+ # lutaml/hash_format/adapter/document.rb).
84
+ #
85
+ # ENTRY_FILES are added unconditionally: they are top-level entry points
86
+ # that nothing else autoloads (because they ARE the entry points users
87
+ # require directly), so they would otherwise be missing from the list.
88
+ ENTRY_FILES = %w[lutaml/model lutaml/xml].freeze
89
+ remaining = (opal_paths - ENTRY_FILES).sort_by { |p| [p.count("/"), p] }
90
+ ordered = ENTRY_FILES + remaining
91
+
92
+ out = []
93
+ out << "# frozen_string_literal: true"
94
+ out << ""
95
+ out << "# DO NOT EDIT — regenerate with:"
96
+ out << "# ruby lib/compat/opal/generate_boot.rb"
97
+ out << "#"
98
+ out << "# Opal does not support Ruby autoload (it is a no-op at parse time)."
99
+ out << "# This file explicitly requires every autoload target in lib/lutaml/**"
100
+ out << "# so that nested constants resolve at runtime under Opal."
101
+ out << "#"
102
+ out << "# Native-only paths excluded under Opal:"
103
+ out << "# - nokogiri/ox/rexml adapters (C extensions unavailable)"
104
+ out << "# - XSD / RELAX NG schema generators (require Nokogiri)"
105
+ out << "# - lutaml/model/cli (thor-based, runtime-irrelevant under Opal)"
106
+ out << "#"
107
+ out << "# Ordering: lutaml/model and lutaml/xml define the Lutaml::Model and"
108
+ out << "# Lutaml::Xml namespaces, so they load first. Remaining paths are"
109
+ out << "# ordered by depth (shallower first) so parent files declare their"
110
+ out << "# child namespaces before children load."
111
+ out << "#"
112
+ out << "# Loaded by the Opal Rakefile before spec_helper."
113
+ out << ""
114
+ ordered.each { |p| out << %(require "#{p}") }
115
+ out << ""
116
+
117
+ # OPAL_BOOT_OUTPUT lets the in-sync guard spec regenerate to a temp path
118
+ # without clobbering the committed manifest; unset, it writes canonically.
119
+ target = ENV.fetch("OPAL_BOOT_OUTPUT") do
120
+ File.expand_path("lutaml_model_boot.rb", __dir__)
121
+ end
122
+ File.write(target, out.join("\n"))
123
+ warn "Generated #{ordered.size} requires (excluded #{native_only.size} native-only paths) -> #{target}"
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # JS bundle entry point. Pulls in the Opal-only compat shims and boot
4
+ # files BEFORE lutaml/model so that moxml's adapter constants, the
5
+ # String/Encoding/YAML/Array#pack patches, and the eager-loaded
6
+ # Moxml::* / Lutaml::* constants are all in place by the time
7
+ # lib/lutaml/model.rb runs.
8
+ #
9
+ # Used by scripts/build.rb as the bundle ENTRY (instead of
10
+ # "lutaml/model" directly) so the JS bundle mirrors the load order
11
+ # that lutaml-model's Rakefile applies for `rake spec:opal`.
12
+
13
+ if RUBY_ENGINE == "opal"
14
+ # 0. runtime_compatibility first: it stubs Mutex/ConditionVariable/
15
+ # Thread/WeakRef and patches Module#prepend for Opal. Oga's LRU
16
+ # and other gems reference Mutex at file-load time, so this must
17
+ # run before any gem that touches thread-safety primitives.
18
+ require "lutaml/model/runtime_compatibility"
19
+
20
+ # 0a. Opal's stdlib StringIO < IO loads here so moxml's
21
+ # `class StringIO` (bare, no parent) in rexml_compat.rb re-opens
22
+ # the existing class instead of conflicting with `< ::IO`.
23
+ # Without this, the bare-class definition runs first and Opal's
24
+ # `< IO` definition later raises "superclass mismatch".
25
+ require "stringio"
26
+
27
+ # 1. stdlib shims: String/Encoding/StringIO + Array#pack + nodejs/yaml
28
+ require "rexml_compat"
29
+ require "yaml_compat"
30
+
31
+ # 2. forks' Opal-aware conditionals select the pure-Ruby lexer
32
+ require "oga"
33
+ require "ll/setup"
34
+
35
+ # 3. eager-load boots (Opal ignores autoload)
36
+ require "moxml_boot"
37
+ require "lutaml_model_boot"
38
+ end
39
+
40
+ # 4. the actual entry point
41
+ require "lutaml/model"
42
+ require "lutaml/xml"