lutaml-model 0.8.17 → 0.8.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/downstream-performance.yml +27 -1
  3. data/.github/workflows/js-pr-check.yml +102 -0
  4. data/.github/workflows/js-sync.yml +51 -0
  5. data/.github/workflows/opal.yml +26 -6
  6. data/.github/workflows/performance.yml +32 -7
  7. data/.github/workflows/rake.yml +78 -32
  8. data/.gitmodules +6 -0
  9. data/.rubocop_todo.yml +8 -0
  10. data/Gemfile +16 -1
  11. data/README.adoc +267 -0
  12. data/Rakefile +166 -4
  13. data/docs/_guides/schema-import.adoc +42 -0
  14. data/docs/_guides/xml-mapping.adoc +21 -0
  15. data/docs/_pages/validation.adoc +65 -0
  16. data/lib/compat/opal/generate_boot.rb +123 -0
  17. data/lib/compat/opal/js_bundle_entry.rb +42 -0
  18. data/lib/compat/opal/lutaml_model_boot.rb +497 -0
  19. data/lib/compat/opal/moxml_boot.rb +68 -0
  20. data/lib/compat/opal/yaml_compat.rb +32 -0
  21. data/lib/lutaml/json.rb +1 -1
  22. data/lib/lutaml/key_value/transform.rb +3 -8
  23. data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -1
  24. data/lib/lutaml/key_value/transformation.rb +17 -5
  25. data/lib/lutaml/model/adapter_resolver.rb +2 -2
  26. data/lib/lutaml/model/attribute.rb +88 -11
  27. data/lib/lutaml/model/cached_type_resolver.rb +1 -1
  28. data/lib/lutaml/model/error/union_schema_unsupported_error.rb +11 -0
  29. data/lib/lutaml/model/global_context.rb +11 -8
  30. data/lib/lutaml/model/instrumentation.rb +2 -2
  31. data/lib/lutaml/model/mapping/mapping_rule.rb +0 -6
  32. data/lib/lutaml/model/register.rb +2 -2
  33. data/lib/lutaml/model/runtime_compatibility.rb +50 -0
  34. data/lib/lutaml/model/schema/class_loader.rb +58 -0
  35. data/lib/lutaml/model/schema/compiled_output.rb +83 -0
  36. data/lib/lutaml/model/schema/definitions/attribute.rb +32 -0
  37. data/lib/lutaml/model/schema/definitions/choice.rb +20 -0
  38. data/lib/lutaml/model/schema/definitions/facet.rb +40 -0
  39. data/lib/lutaml/model/schema/definitions/group_import.rb +22 -0
  40. data/lib/lutaml/model/schema/definitions/member_walk.rb +33 -0
  41. data/lib/lutaml/model/schema/definitions/model.rb +40 -0
  42. data/lib/lutaml/model/schema/definitions/namespace.rb +23 -0
  43. data/lib/lutaml/model/schema/definitions/restricted_type.rb +30 -0
  44. data/lib/lutaml/model/schema/definitions/sequence.rb +19 -0
  45. data/lib/lutaml/model/schema/definitions/simple_content.rb +22 -0
  46. data/lib/lutaml/model/schema/definitions/transform_facet.rb +21 -0
  47. data/lib/lutaml/model/schema/definitions/type_ref.rb +20 -0
  48. data/lib/lutaml/model/schema/definitions/union_type.rb +28 -0
  49. data/lib/lutaml/model/schema/definitions/xml_root.rb +20 -0
  50. data/lib/lutaml/model/schema/definitions.rb +24 -0
  51. data/lib/lutaml/model/schema/file_writer.rb +70 -0
  52. data/lib/lutaml/model/schema/generator/definitions_collection.rb +16 -2
  53. data/lib/lutaml/model/schema/generator/property.rb +21 -4
  54. data/lib/lutaml/model/schema/module_nesting.rb +31 -0
  55. data/lib/lutaml/model/schema/namespace_naming.rb +46 -0
  56. data/lib/lutaml/model/schema/registry_generator.rb +115 -0
  57. data/lib/lutaml/model/schema/renderers/base.rb +40 -0
  58. data/lib/lutaml/model/schema/renderers/mappings.rb +72 -0
  59. data/lib/lutaml/model/schema/renderers/member_decls.rb +115 -0
  60. data/lib/lutaml/model/schema/renderers/model.rb +121 -0
  61. data/lib/lutaml/model/schema/renderers/namespace.rb +30 -0
  62. data/lib/lutaml/model/schema/renderers/registration.rb +64 -0
  63. data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +93 -0
  64. data/lib/lutaml/model/schema/renderers/restricted_type.rb +88 -0
  65. data/lib/lutaml/model/schema/renderers/union.rb +83 -0
  66. data/lib/lutaml/model/schema/renderers.rb +19 -0
  67. data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +47 -0
  68. data/lib/lutaml/model/schema/rnc_compiler.rb +62 -0
  69. data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +91 -0
  70. data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +392 -0
  71. data/lib/lutaml/model/schema/rng_compiler/member_collector.rb +30 -0
  72. data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +152 -0
  73. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +125 -0
  74. data/lib/lutaml/model/schema/rng_compiler.rb +220 -0
  75. data/lib/lutaml/model/schema/templates.rb +162 -0
  76. data/lib/lutaml/model/schema/xml_compiler/element_order.rb +41 -0
  77. data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +31 -107
  78. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +191 -0
  79. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +216 -0
  80. data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +153 -0
  81. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +167 -0
  82. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +87 -0
  83. data/lib/lutaml/model/schema/xml_compiler.rb +38 -517
  84. data/lib/lutaml/model/schema.rb +19 -0
  85. data/lib/lutaml/model/serialize/attribute_definition.rb +14 -2
  86. data/lib/lutaml/model/serialize/builder.rb +15 -2
  87. data/lib/lutaml/model/serialize/value_mapping.rb +9 -57
  88. data/lib/lutaml/model/serialize.rb +6 -9
  89. data/lib/lutaml/model/services/rule_value_extractor.rb +2 -3
  90. data/lib/lutaml/model/services.rb +0 -2
  91. data/lib/lutaml/model/store.rb +2 -1
  92. data/lib/lutaml/model/toml.rb +1 -1
  93. data/lib/lutaml/model/transform.rb +17 -39
  94. data/lib/lutaml/model/transformation_registry.rb +1 -1
  95. data/lib/lutaml/model/type.rb +1 -0
  96. data/lib/lutaml/model/union.rb +320 -0
  97. data/lib/lutaml/model/utils.rb +6 -0
  98. data/lib/lutaml/model/validation.rb +1 -4
  99. data/lib/lutaml/model/version.rb +1 -1
  100. data/lib/lutaml/model.rb +18 -0
  101. data/lib/lutaml/toml.rb +1 -1
  102. data/lib/lutaml/xml/adapter.rb +1 -1
  103. data/lib/lutaml/xml/adapter_loader.rb +1 -1
  104. data/lib/lutaml/xml/builder/base.rb +1 -1
  105. data/lib/lutaml/xml/builder.rb +1 -1
  106. data/lib/lutaml/xml/error/schema_validation_error.rb +25 -0
  107. data/lib/lutaml/xml/model_transform.rb +5 -1
  108. data/lib/lutaml/xml/schema/relaxng_schema.rb +5 -0
  109. data/lib/lutaml/xml/schema/xsd_schema.rb +4 -0
  110. data/lib/lutaml/xml/serialization/format_conversion.rb +48 -0
  111. data/lib/lutaml/xml/serialization/instance_methods.rb +13 -0
  112. data/lib/lutaml/xml/transformation/element_builder.rb +21 -8
  113. data/lib/lutaml/xml/transformation/ordered_applier.rb +6 -0
  114. data/lib/lutaml/xml/xsd_validator.rb +66 -0
  115. data/lib/lutaml/xml.rb +53 -12
  116. data/lutaml-model.gemspec +4 -2
  117. data/spec/fixtures/xml/schema/rnc/address_book.rnc +15 -0
  118. data/spec/fixtures/xml/schema/rnc/book_features.rnc +18 -0
  119. data/spec/fixtures/xml/schema/rnc/includes/book.rnc +6 -0
  120. data/spec/fixtures/xml/schema/rnc/includes/circular_a.rnc +8 -0
  121. data/spec/fixtures/xml/schema/rnc/includes/circular_b.rnc +8 -0
  122. data/spec/fixtures/xml/schema/rnc/includes/library.rnc +8 -0
  123. data/spec/fixtures/xml/schema/rng/address_book.rng +16 -0
  124. data/spec/fixtures/xml/schema/rng/book.rng +27 -0
  125. data/spec/fixtures/xml/schema/rng/fixed_value.rng +11 -0
  126. data/spec/fixtures/xml/schema/rng/fragment_in_choice.rng +30 -0
  127. data/spec/fixtures/xml/schema/rng/full_name.rng +39 -0
  128. data/spec/fixtures/xml/schema/rng/integer_range.rng +32 -0
  129. data/spec/fixtures/xml/schema/rng/list_type.rng +12 -0
  130. data/spec/fixtures/xml/schema/rng/namespaced.rng +9 -0
  131. data/spec/fixtures/xml/schema/rng/paragraph.rng +13 -0
  132. data/spec/fixtures/xml/schema/rng/person.rng +23 -0
  133. data/spec/fixtures/xml/schema/rng/union.rng +14 -0
  134. data/spec/fixtures/xml/validate_xml_with_address.xsd +13 -0
  135. data/spec/fixtures/xml/validate_xml_with_contact.xsd +11 -0
  136. data/spec/fixtures/xml/validate_xml_with_person.xsd +11 -0
  137. data/spec/fixtures/xml/validate_xml_with_person_strict.xsd +17 -0
  138. data/spec/fixtures/xml/validate_xml_with_product.xsd +37 -0
  139. data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
  140. data/spec/lutaml/model/attribute_spec.rb +169 -12
  141. data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
  142. data/spec/lutaml/model/collection_spec.rb +18 -0
  143. data/spec/lutaml/model/mixed_content_spec.rb +171 -0
  144. data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
  145. data/spec/lutaml/model/ordered_content_spec.rb +30 -0
  146. data/spec/lutaml/model/raw_element_spec.rb +125 -0
  147. data/spec/lutaml/model/rule_value_extractor_spec.rb +7 -14
  148. data/spec/lutaml/model/runtime_compatibility_spec.rb +25 -0
  149. data/spec/lutaml/model/schema/renderers/model_spec.rb +149 -0
  150. data/spec/lutaml/model/schema/renderers/namespace_spec.rb +36 -0
  151. data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +75 -0
  152. data/spec/lutaml/model/schema/renderers/union_spec.rb +58 -0
  153. data/spec/lutaml/model/transform_apply_value_map_spec.rb +64 -0
  154. data/spec/lutaml/model/union_attribute_spec.rb +658 -0
  155. data/spec/lutaml/model/union_spec.rb +257 -0
  156. data/spec/lutaml/model/utils_spec.rb +14 -0
  157. data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
  158. data/spec/lutaml/xml/schema/compiler_spec.rb +21 -4
  159. data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +404 -0
  160. data/spec/lutaml/xml/schema/rng_compiler_spec.rb +893 -0
  161. data/spec/lutaml/xml/validate_xml_with_constructs_spec.rb +222 -0
  162. data/spec/lutaml/xml/validate_xml_with_spec.rb +186 -0
  163. data/spec/spec_helper.rb +28 -11
  164. data/spec/support/opal.rb +5 -2
  165. metadata +97 -31
  166. data/lib/lutaml/model/schema/xml_compiler/attribute.rb +0 -106
  167. data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +0 -47
  168. data/lib/lutaml/model/schema/xml_compiler/choice.rb +0 -67
  169. data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +0 -27
  170. data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +0 -34
  171. data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +0 -173
  172. data/lib/lutaml/model/schema/xml_compiler/element.rb +0 -121
  173. data/lib/lutaml/model/schema/xml_compiler/group.rb +0 -113
  174. data/lib/lutaml/model/schema/xml_compiler/restriction.rb +0 -104
  175. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -52
  176. data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +0 -40
  177. data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +0 -265
  178. data/lib/lutaml/model/schema/xml_compiler/xml_namespace_class.rb +0 -110
  179. data/lib/lutaml/model/services/default_value_resolver.rb +0 -60
  180. data/spec/lutaml/model/services/default_value_resolver_spec.rb +0 -162
  181. data/spec/lutaml/xml/schema/compiler/attribute_group_spec.rb +0 -71
  182. data/spec/lutaml/xml/schema/compiler/attribute_spec.rb +0 -67
  183. data/spec/lutaml/xml/schema/compiler/choice_spec.rb +0 -74
  184. data/spec/lutaml/xml/schema/compiler/complex_content_restriction_spec.rb +0 -60
  185. data/spec/lutaml/xml/schema/compiler/complex_content_spec.rb +0 -41
  186. data/spec/lutaml/xml/schema/compiler/complex_type_spec.rb +0 -202
  187. data/spec/lutaml/xml/schema/compiler/element_spec.rb +0 -68
  188. data/spec/lutaml/xml/schema/compiler/group_spec.rb +0 -90
  189. data/spec/lutaml/xml/schema/compiler/restriction_spec.rb +0 -77
  190. data/spec/lutaml/xml/schema/compiler/sequence_spec.rb +0 -65
  191. data/spec/lutaml/xml/schema/compiler/simple_content_spec.rb +0 -61
  192. data/spec/lutaml/xml/schema/compiler/simple_type_spec.rb +0 -184
data/README.adoc CHANGED
@@ -303,6 +303,42 @@ gem install lutaml-model
303
303
 
304
304
  Lutaml::Model includes a command line interface (CLI) that provides useful tools for working with your data models. The CLI is available through the `lutaml-model` command after installation.
305
305
 
306
+ == Opal (JavaScript) support
307
+
308
+ Lutaml::Model runs under
309
+ https://opalrb.com/[Opal] (Ruby compiled to JavaScript), which
310
+ powers the
311
+ https://www.npmjs.com/package/@lutaml/lutaml-model[`@lutaml/lutaml-model`]
312
+ npm package. Two pure-Ruby XML adapters ship under Opal:
313
+
314
+ * **Oga** (default) — compiled from the vendored `opal-oga` fork,
315
+ which provides a pure-Ruby lexer.
316
+ * **REXML** (opt-in) — compiled from the bundled stdlib gem, with
317
+ moxml's `lib/compat/opal/rexml/*` shadows patching the parts Opal
318
+ can't follow natively.
319
+
320
+ Nokogiri and Ox are C extensions and are not available under Opal.
321
+
322
+ [source,ruby]
323
+ ----
324
+ # Default (Oga)
325
+ Lutaml::Model::Config.xml_adapter_type = :oga
326
+
327
+ # Opt-in to REXML
328
+ Lutaml::Model::Config.xml_adapter_type = :rexml
329
+ ----
330
+
331
+ The Opal smoke specs in `spec/lutaml/xml/opal_xml_spec.rb` run a
332
+ shared XML round-trip contract against BOTH adapters so a regression
333
+ in either is caught at CI time (`bundle exec rake spec:opal`).
334
+
335
+ The npm package `@lutaml/lutaml-model` is rebuilt automatically by
336
+ the `lutaml-model-js` repo on every push to this repo's `main` branch
337
+ (publishes under the `next` dist-tag) and on every stable gem release
338
+ (publishes under `latest`). See
339
+ https://github.com/lutaml/lutaml-model-js[`lutaml-model-js`] for
340
+ details.
341
+
306
342
  === Compare Command
307
343
 
308
344
  The `compare` command allows you to compare two data files of different formats using your Lutaml::Model classes. This is particularly useful for:
@@ -2657,6 +2693,112 @@ references:
2657
2693
 
2658
2694
 
2659
2695
 
2696
+ === Union attributes
2697
+
2698
+ ==== General
2699
+
2700
+ A union attribute accepts a value conforming to *one of several declared
2701
+ types* -- the functional equivalent of `xsd:union`. Declare the member types
2702
+ as a bare array in the type position, in priority order:
2703
+
2704
+ [source,ruby]
2705
+ ----
2706
+ class Ceramic < Lutaml::Model::Serializable
2707
+ attribute :firing_temperature, [TemperatureWithUnit, Temperature, :string]
2708
+
2709
+ xml do
2710
+ root "ceramic"
2711
+ map_element "FiringTemperature", to: :firing_temperature
2712
+ end
2713
+
2714
+ key_value do
2715
+ map "firing_temperature", to: :firing_temperature
2716
+ end
2717
+ end
2718
+ ----
2719
+
2720
+ Members may be model classes (`Lutaml::Model::Serializable`) or one of the
2721
+ supported scalar value types: `:string`, `:integer`, `:float`, `:decimal`,
2722
+ `:boolean`. Other value types (e.g. `:date`, `:date_time`, `:time`, `:symbol`,
2723
+ `:uri`) and option hashes such as `{ ref: [...] }` are not supported as union
2724
+ members and are rejected at definition time — see Limitations.
2725
+
2726
+ ==== Semantics
2727
+
2728
+ Resolution follows `xsd:union`: the value is matched against the members in
2729
+ *declared order*, and the *first member it conforms to wins*. A value is valid
2730
+ if it conforms to *at least one* member -- there is no "exactly one" rule.
2731
+
2732
+ Conformance is by lexical space for scalars and by key coverage for models:
2733
+
2734
+ * A scalar string conforms to `:integer` only if it is an integer literal
2735
+ (`"42"`), to `:float` only if it parses as a finite number, and to
2736
+ `:boolean` only for `true`/`false`. So `[:integer, :string]` resolves `"42"`
2737
+ to the integer `42` but `"hello"` to the string `"hello"`.
2738
+ * A structured value conforms to a model member when its keys (hash keys, or
2739
+ XML child-element and attribute names) are covered by that member's mapped
2740
+ fields.
2741
+
2742
+ A universal catch-all (`:string`/`String`) must be the *last* member, so
2743
+ earlier scalar members stay reachable. This is enforced at definition time.
2744
+
2745
+ ==== No-match handling
2746
+
2747
+ A value that matches no member becomes `nil`, exactly like any other failed
2748
+ cast in the library. Use `required: true` for strictness -- a no-match then
2749
+ surfaces the standard `RequiredAttributeMissingError` during validation:
2750
+
2751
+ [source,ruby]
2752
+ ----
2753
+ class Reading < Lutaml::Model::Serializable
2754
+ attribute :value, [:integer, :float], required: true
2755
+ key_value { map "value", to: :value }
2756
+ end
2757
+
2758
+ Reading.new(value: "not a number").value # => nil
2759
+ Reading.new(value: "not a number").validate! # raises (RequiredAttributeMissingError)
2760
+ ----
2761
+
2762
+ There is no `strict:` flag; strictness uses the existing `required:` mechanism.
2763
+
2764
+ ==== Serialization
2765
+
2766
+ Serialization is stateless and driven by the value's own class, so a member
2767
+ built in plain Ruby serializes identically to a deserialized one. The same
2768
+ element/key name is used for every member; XML distinguishes members by shape
2769
+ (text-only for scalars, child elements/attributes for models):
2770
+
2771
+ [source,xml]
2772
+ ----
2773
+ <FiringTemperature>Very Hot</FiringTemperature>
2774
+ <FiringTemperature><number>1300</number><unit>C</unit></FiringTemperature>
2775
+ ----
2776
+
2777
+ ==== Collections
2778
+
2779
+ A `collection: true` union resolves each element independently, mixing model
2780
+ and scalar members in one collection.
2781
+
2782
+ ==== Limitations
2783
+
2784
+ * JSON Schema export emits `anyOf`. XSD and RelaxNG export raise
2785
+ `UnionSchemaUnsupportedError` -- a union of structured members has no faithful
2786
+ representation in those schema languages.
2787
+ * `union` cannot be combined with `polymorphic`, `raw`, `with`, or
2788
+ `child_mappings` (rejected at definition time).
2789
+ * Differently-named or namespaced XML members are not supported; members share
2790
+ one element name and are distinguished by shape and key coverage.
2791
+ * Scalar members are limited to `:string`, `:integer`, `:float`, `:decimal`,
2792
+ `:boolean` (plus any `Serializable` model). Other value types are rejected at
2793
+ definition time because the stateless, `value.class`-driven serialization
2794
+ cannot recover them faithfully — e.g. `Type::Date.cast` can yield a `DateTime`
2795
+ (so `:date`/`:date_time` are indistinguishable), `:time` and
2796
+ `:time_without_date` share a Ruby `Time` class, and `:hash` is ambiguous with
2797
+ model members for structured input.
2798
+ * Reference (`{ ref: [...] }`) or other option-hash members are not supported.
2799
+
2800
+
2801
+
2660
2802
  === Collection attributes
2661
2803
 
2662
2804
  Define attributes as collections (arrays or hashes) to store multiple values
@@ -16444,6 +16586,7 @@ models. This allows you to create models from existing schema definitions.
16444
16586
  Currently, the following schema formats are supported:
16445
16587
 
16446
16588
  * XSD (https://w3.org/TR/xmlschema-1/[XML Schema Definition Language])
16589
+ * RELAX NG (https://relaxng.org/[RELAX NG], see <<rng-schema-compilation>>)
16447
16590
  * JSON/YAML Schema
16448
16591
 
16449
16592
  Please refer to link:docs/schema_import.adoc[Schema Import] for details
@@ -17332,6 +17475,130 @@ end
17332
17475
  For a complete architecture overview and migration guide from Register to
17333
17476
  GlobalContext, see link:docs/architecture.md[Architecture Documentation].
17334
17477
 
17478
+ [[rng-schema-compilation]]
17479
+ == RELAX NG Schema Compilation
17480
+
17481
+ Lutaml::Model provides functionality to compile RELAX NG (RNG) schemas into
17482
+ Ruby model classes, analogous to <<schema-import,XSD schema import>>.
17483
+
17484
+ === Prerequisite: the `rng` gem
17485
+
17486
+ Unlike XSD support, RNG schema parsing is provided by an external gem.
17487
+ Lutaml::Model itself does not depend on it -- it is only loaded when
17488
+ `Schema.from_relaxng` is actually invoked.
17489
+
17490
+ To use `Schema.from_relaxng`, add the https://github.com/lutaml/rng[rng] gem to
17491
+ your `Gemfile`:
17492
+
17493
+ [source,ruby]
17494
+ ----
17495
+ gem "rng"
17496
+ ----
17497
+
17498
+ Then require it in your application:
17499
+
17500
+ [source,ruby]
17501
+ ----
17502
+ require "lutaml/model"
17503
+ require "rng" # provides Rng::Grammar, needed by Schema.from_relaxng
17504
+ ----
17505
+
17506
+ Calling `Schema.from_relaxng` without the rng gem loaded raises:
17507
+
17508
+ ----
17509
+ RNG schema compilation requires the rng gem. Add `gem "rng"` to your Gemfile.
17510
+ ----
17511
+
17512
+ === Basic usage
17513
+
17514
+ [source,ruby]
17515
+ ----
17516
+ sources = Lutaml::Model::Schema.from_relaxng(rng_content)
17517
+ # => { "AddressBook" => "...", "Card" => "...", ... }
17518
+ ----
17519
+
17520
+ By default, `from_relaxng` returns a Hash mapping class name to generated Ruby
17521
+ source. The `options` hash accepts the same keys as `from_xml`:
17522
+
17523
+ `output_dir`::: The directory where the model files will be saved. If not
17524
+ provided, a default directory named `rng_models_<timestamp>` is created.
17525
+ +
17526
+ [example]
17527
+ `"path/to/directory"`
17528
+
17529
+ `create_files`::: A `boolean` argument (`false` by default) to create files directly in the specified directory as defined by the `output_dir` option.
17530
+ +
17531
+ [example]
17532
+ `create_files: (true | false)`
17533
+
17534
+ `load_classes`::: A `boolean` argument (`false` by default) to load generated classes before returning them.
17535
+ +
17536
+ [example]
17537
+ `load_classes: (true | false)`
17538
+
17539
+ `module_namespace`::: The Ruby module name that wraps generated classes (e.g.
17540
+ `"MyModels"`). If omitted, no wrapping module is generated.
17541
+ +
17542
+ [example]
17543
+ `module_namespace: "MyModels"`
17544
+
17545
+ NOTE: If both `create_files` and `load_classes` are provided, the `create_files`
17546
+ argument will take priority and generate files without loading them!
17547
+
17548
+ [example]
17549
+ .Using `Lutaml::Model::Schema#from_relaxng` to convert an RNG schema to model files
17550
+ ====
17551
+ [source,ruby]
17552
+ ----
17553
+ options = {
17554
+ # These are all optional:
17555
+ output_dir: "lib/my_models",
17556
+ module_namespace: "MyModels",
17557
+ create_files: true, # Default: false
17558
+ # OR
17559
+ load_classes: true, # Default: false
17560
+ }
17561
+
17562
+ Lutaml::Model::Schema.from_relaxng(rng_content, options: options)
17563
+ ----
17564
+ ====
17565
+
17566
+ You could also directly load the generated Ruby files into your application by
17567
+ requiring the generated registry.
17568
+
17569
+ [example]
17570
+ .Using the generated Ruby files in your application
17571
+ ====
17572
+ [source,ruby]
17573
+ ----
17574
+ Lutaml::Model::Schema.from_relaxng(
17575
+ rng_content,
17576
+ options: { output_dir: "lib/my_models", create_files: true, module_namespace: "MyModels" }
17577
+ )
17578
+ require "lib/my_models/mymodels_registry"
17579
+ MyModels.register_all
17580
+
17581
+ MyModels::AddressBook.from_xml(xml_content)
17582
+ ----
17583
+ ====
17584
+
17585
+ === Supported RNG constructs
17586
+
17587
+ * `<grammar>`, `<start>`, `<define>`
17588
+ * `<element>` (nested and ref-targeted)
17589
+ * `<attribute>` -> `map_attribute`
17590
+ * `<ref>` -- resolves wrapping defines to typed attributes; resolves
17591
+ fragment defines (multi-element or attribute-group) to `import_model`
17592
+ * `<optional>`, `<zeroOrMore>`, `<oneOrMore>`, `<group>`, `<choice>`
17593
+ * `<text/>`, `<data type="..."/>` (string/integer/boolean/float/decimal/date/dateTime/time)
17594
+ * `<empty/>`, `<mixed>`
17595
+
17596
+ === Round-trip with RELAX NG generation
17597
+
17598
+ `Schema.from_relaxng` is round-trip-compatible with
17599
+ `Lutaml::Xml::Schema::RelaxngSchema.generate`: taking a model, generating
17600
+ RNG from it, and compiling the RNG back yields equivalent class source.
17601
+
17335
17602
  [[document-validation-framework]]
17336
17603
  == Document Validation Framework
17337
17604
 
data/Rakefile CHANGED
@@ -1,14 +1,176 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
4
 
6
- RSpec::Core::RakeTask.new(:spec)
5
+ # vendor:prepare must be runnable before `bundle install` (CI runs it
6
+ # first so the path-source oga/ruby-ll forks' gitignored lexer/parser
7
+ # outputs exist before their extconf.rb runs). Guard the rspec/opal
8
+ # requires so the file loads with only `rake` + `bundler` available.
9
+ begin
10
+ require "rspec/core/rake_task"
11
+ RSpec::Core::RakeTask.new(:spec)
12
+ rescue LoadError
13
+ end
7
14
 
8
- require "rubocop/rake_task"
15
+ begin
16
+ require "opal/rspec/rake_task"
9
17
 
10
- RuboCop::RakeTask.new
18
+ # Opal-compatible overrides need to be on Opal's global load path so
19
+ # the compiler can follow `require "lutaml_model_boot"` etc.
20
+ # moxml ships its own compat files (`lib/compat/opal/`) inside the gem;
21
+ # we add moxml's gem dir to Opal's load path so we can reuse them.
22
+ # moxml's lib root is also added so `require "moxml"` resolves.
23
+ #
24
+ # The REXML gem is a bundled Ruby stdlib gem (pure Ruby). moxml's
25
+ # lib/compat/opal/rexml/* shadows patch its namespace/text/source/etc.
26
+ # for Opal. For `require "rexml/document"` and friends to resolve, the
27
+ # gem's lib dir must be on Opal's load path (mirrors what moxml does).
28
+ if defined?(Opal)
29
+ Opal.append_path File.expand_path("lib/compat/opal", __dir__)
30
+
31
+ moxml_gem_dir = Gem::Specification.find_by_name("moxml")&.gem_dir
32
+ if moxml_gem_dir
33
+ Opal.append_path File.join(moxml_gem_dir, "lib")
34
+ Opal.append_path File.join(moxml_gem_dir, "lib/compat/opal")
35
+ end
36
+
37
+ rexml_lib = $LOAD_PATH.find do |p|
38
+ File.exist?(File.join(p, "rexml", "document.rb"))
39
+ end
40
+ Opal.append_path rexml_lib if rexml_lib
41
+
42
+ # The Opal-compatible oga and ruby-ll forks (vendored as submodules)
43
+ # expose pure-Ruby implementations under ext/pureruby/. Their top-level
44
+ # lib/oga.rb and lib/ll/setup.rb conditionally require them when
45
+ # RUBY_PLATFORM == 'opal'. Both lib/ and ext/pureruby/ must be on
46
+ # Opal's load path so the conditional resolves correctly.
47
+ %w[opal-oga opal-ruby-ll].each do |fork_name|
48
+ fork_path = File.expand_path("vendor/#{fork_name}", __dir__)
49
+ Opal.append_path File.join(fork_path, "lib")
50
+ Opal.append_path File.join(fork_path, "ext/pureruby")
51
+ end
52
+ end
53
+ rescue LoadError
54
+ # Opal not available or incompatible with current Ruby version
55
+ end
56
+
57
+ begin
58
+ require "rubocop/rake_task"
59
+ RuboCop::RakeTask.new
60
+ rescue LoadError
61
+ end
11
62
 
12
63
  Dir.glob("lib/tasks/**/*.rake").each { |r| load r }
13
64
 
65
+ # Regenerate the ragel / ruby-ll outputs that the Opal-compatible forks
66
+ # (vendored as submodules under vendor/) gitignore. The forks ship the
67
+ # grammar sources (.rl / .rll) but not the generated .rb / .c, since
68
+ # those are large and version-controllable upstream. Both ragel and
69
+ # ruby-ll must be on PATH; the upstream ruby-ll gem is sufficient for
70
+ # generation (the fork is only needed at runtime).
71
+ namespace :vendor do
72
+ desc "Generate ragel / ruby-ll outputs in vendored opal-oga and opal-ruby-ll"
73
+ task :prepare do
74
+ require "fileutils"
75
+
76
+ oga = File.expand_path("vendor/opal-oga", __dir__)
77
+ ruby_ll = File.expand_path("vendor/opal-ruby-ll", __dir__)
78
+
79
+ generators = [
80
+ # oga: ruby-ll grammar -> Ruby parser
81
+ ["ruby-ll #{oga}/lib/oga/xml/parser.rll -o #{oga}/lib/oga/xml/parser.rb",
82
+ "#{oga}/lib/oga/xml/parser.rb",
83
+ "#{oga}/lib/oga/xml/parser.rll"],
84
+ ["ruby-ll #{oga}/lib/oga/xpath/parser.rll -o #{oga}/lib/oga/xpath/parser.rb",
85
+ "#{oga}/lib/oga/xpath/parser.rb",
86
+ "#{oga}/lib/oga/xpath/parser.rll"],
87
+ ["ruby-ll #{oga}/lib/oga/css/parser.rll -o #{oga}/lib/oga/css/parser.rb",
88
+ "#{oga}/lib/oga/css/parser.rb",
89
+ "#{oga}/lib/oga/css/parser.rll"],
90
+ # oga: ragel Ruby lexer
91
+ ["ragel -R -F1 #{oga}/lib/oga/xpath/lexer.rl -o #{oga}/lib/oga/xpath/lexer.rb",
92
+ "#{oga}/lib/oga/xpath/lexer.rb",
93
+ "#{oga}/lib/oga/xpath/lexer.rl"],
94
+ ["ragel -R -F1 #{oga}/lib/oga/css/lexer.rl -o #{oga}/lib/oga/css/lexer.rb",
95
+ "#{oga}/lib/oga/css/lexer.rb",
96
+ "#{oga}/lib/oga/css/lexer.rl"],
97
+ # oga: ragel C lexer for liboga
98
+ ["ragel -C -I #{oga}/ext/ragel -G2 #{oga}/ext/c/lexer.rl -o #{oga}/ext/c/lexer.c",
99
+ "#{oga}/ext/c/lexer.c",
100
+ "#{oga}/ext/c/lexer.rl"],
101
+ # ruby-ll: ruby-ll grammar -> Ruby parser
102
+ ["ruby-ll #{ruby_ll}/lib/ll/parser.rll -o #{ruby_ll}/lib/ll/parser.rb --no-requires",
103
+ "#{ruby_ll}/lib/ll/parser.rb",
104
+ "#{ruby_ll}/lib/ll/parser.rll"],
105
+ ]
106
+
107
+ generators.each do |cmd, output, source|
108
+ if File.exist?(output) && File.mtime(output) >= File.mtime(source)
109
+ next
110
+ end
111
+
112
+ FileUtils.mkdir_p(File.dirname(output))
113
+ sh cmd
114
+ end
115
+ end
116
+
117
+ # Bundler does not reliably compile native extensions for path-source
118
+ # gems. Build liboga/libll explicitly so `require "oga"` resolves.
119
+ desc "Compile liboga / libll native extensions for vendored forks"
120
+ task :compile do
121
+ require "fileutils"
122
+ require "rbconfig"
123
+
124
+ dlext = RbConfig::CONFIG["DLEXT"]
125
+
126
+ {
127
+ "vendor/opal-ruby-ll" => "libll",
128
+ "vendor/opal-oga" => "liboga",
129
+ }.each do |fork_path, ext_name|
130
+ abs_fork = File.expand_path(fork_path, __dir__)
131
+ lib_bundle = File.join(abs_fork, "lib", "#{ext_name}.#{dlext}")
132
+ ext_dir = File.join(abs_fork, "ext", "c")
133
+ extconf = File.join(ext_dir, "extconf.rb")
134
+ lib_dir = File.join(abs_fork, "lib")
135
+ next if File.exist?(lib_bundle)
136
+
137
+ Dir.chdir(ext_dir) do
138
+ sh "ruby #{extconf}"
139
+ sh "make"
140
+ FileUtils.cp("#{ext_name}.#{dlext}", lib_dir)
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ if defined?(RSpec)
147
+ namespace :spec do
148
+ if defined?(Opal::RSpec::RakeTask)
149
+ desc "Run Opal (JavaScript) tests"
150
+ Opal::RSpec::RakeTask.new(:opal) do |server, runner|
151
+ server.append_path "lib"
152
+ server.append_path "spec"
153
+
154
+ runner.default_path = "spec"
155
+ # Load order matters:
156
+ # rexml_compat — moxml stdlib shim (String/Encoding/StringIO) required by Oga
157
+ # yaml_compat — adds YAML.safe_load / YAML.dump on top of Opal's nodejs/yaml
158
+ # oga, ll/setup — forks' Opal-aware conditionals select pure-Ruby lexer
159
+ # moxml_boot — eager-loads Moxml::* (lutaml-model's copy, skips broken REXML paths in moxml 0.1.25)
160
+ # lutaml_model_boot — eager-loads Lutaml::* (Opal ignores autoload)
161
+ # spec_helper, support/opal — test infrastructure
162
+ runner.requires = %w[rexml_compat yaml_compat
163
+ oga ll/setup
164
+ moxml_boot
165
+ lutaml_model_boot
166
+ spec_helper support/opal]
167
+ runner.files = Dir.glob("spec/lutaml/**/opal*_spec.rb")
168
+ end
169
+
170
+ desc "Alias for spec:opal that also runs vendor:prepare"
171
+ task opal: "vendor:prepare"
172
+ end
173
+ end
174
+ end
175
+
14
176
  task default: %i[spec rubocop]
@@ -59,6 +59,8 @@ create corresponding LutaML models.
59
59
  Currently, the following schema formats are supported for import:
60
60
 
61
61
  * XSD (https://w3.org/TR/xmlschema-1/[XML Schema Definition Language])
62
+ * RELAX NG XML syntax (RNG)
63
+ * RELAX NG compact syntax (RNC)
62
64
  * JSON Schema (https://json-schema.org/[JSON Schema specification])
63
65
  * YAML Schema (YAML-based JSON Schema definitions)
64
66
 
@@ -235,6 +237,46 @@ require_relative 'path/to/directory/*.rb'
235
237
 
236
238
  For detailed XSD schema parsing (inspecting and manipulating XSD schemas as Ruby objects, including round-tripping), see the link:./_references/xsd-schema-parsing.adoc[XSD Schema Parsing Reference].
237
239
 
240
+ === RELAX NG import
241
+
242
+ Lutaml::Model supports importing RELAX NG XML syntax (`*.rng`) and RELAX NG
243
+ compact syntax (`*.rnc`) schemas into Ruby model classes.
244
+
245
+ RNC import is implemented as a thin adapter over RNG import:
246
+
247
+ [source]
248
+ ----
249
+ RNC source -> Rng::Grammar -> RngCompiler -> Lutaml::Model classes
250
+ ----
251
+
252
+ The `rng` gem provides RNC/RNG parsing and must be available before using
253
+ these APIs.
254
+
255
+ [source,ruby]
256
+ ----
257
+ require "lutaml/xml"
258
+ require "rng"
259
+
260
+ rng_sources = Lutaml::Model::Schema.from_relaxng(rng_content)
261
+ rnc_sources = Lutaml::Model::Schema.from_rnc(rnc_content)
262
+ ----
263
+
264
+ Both methods return a hash mapping generated class names to Ruby source by
265
+ default. They also support the same file-generation options as XSD import:
266
+
267
+ [source,ruby]
268
+ ----
269
+ Lutaml::Model::Schema.from_rnc(
270
+ File.read("reference-docs/v3.rnc"),
271
+ location: "reference-docs/v3.rnc",
272
+ output_dir: "lib/rfcxml",
273
+ create_files: true,
274
+ module_namespace: "RfcXml",
275
+ )
276
+ ----
277
+
278
+ When an RNC schema contains local `include "..."` directives, pass `location:`
279
+ as either the source file path or the directory used to resolve those includes.
238
280
 
239
281
  === JSON Schema import
240
282
 
@@ -1172,6 +1172,27 @@ class MultiFragment < Lutaml::Model::Serializable
1172
1172
  end
1173
1173
  end
1174
1174
  ----
1175
+
1176
+ `raw: :element` also works on `mixed_content` and `ordered` parents. The
1177
+ captured string is emitted verbatim regardless of which applier dispatches the
1178
+ rule. This matters for round-trip serialization: when a model is parsed from
1179
+ XML, `element_order` is populated and the `OrderedApplier` path takes over --
1180
+ without the `raw: :element` short-circuit, each captured fragment (which already
1181
+ contains its own wrapper tag) would be wrapped again, producing
1182
+ `<fragment><fragment>...</fragment></fragment>`.
1183
+
1184
+ [source,ruby]
1185
+ ----
1186
+ class WordParagraph < Lutaml::Model::Serializable
1187
+ attribute :runs, :string, collection: true
1188
+
1189
+ xml do
1190
+ element "p"
1191
+ mixed_content
1192
+ map_element "oMath", to: :runs, raw: :element
1193
+ end
1194
+ end
1195
+ ----
1175
1196
  ====
1176
1197
 
1177
1198
  ===== `raw: :content` on `map_element` -- inner content capture
@@ -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