lutaml-model 0.8.17 → 0.8.19
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.
- checksums.yaml +4 -4
- data/.github/workflows/downstream-performance.yml +27 -1
- data/.github/workflows/js-pr-check.yml +102 -0
- data/.github/workflows/js-sync.yml +51 -0
- data/.github/workflows/opal.yml +26 -6
- data/.github/workflows/performance.yml +32 -7
- data/.github/workflows/rake.yml +78 -32
- data/.gitmodules +6 -0
- data/.rubocop_todo.yml +8 -0
- data/Gemfile +16 -1
- data/README.adoc +267 -0
- data/Rakefile +166 -4
- data/docs/_guides/builder-dsl.adoc +127 -0
- data/docs/_guides/schema-import.adoc +42 -0
- data/docs/_guides/xml-mapping.adoc +21 -0
- data/docs/_pages/validation.adoc +65 -0
- data/lib/compat/opal/generate_boot.rb +123 -0
- data/lib/compat/opal/js_bundle_entry.rb +42 -0
- data/lib/compat/opal/lutaml_model_boot.rb +497 -0
- data/lib/compat/opal/moxml_boot.rb +68 -0
- data/lib/compat/opal/yaml_compat.rb +32 -0
- data/lib/lutaml/json.rb +1 -1
- data/lib/lutaml/key_value/transform.rb +3 -8
- data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -1
- data/lib/lutaml/key_value/transformation.rb +17 -5
- data/lib/lutaml/model/adapter_resolver.rb +2 -2
- data/lib/lutaml/model/attribute.rb +88 -11
- data/lib/lutaml/model/cached_type_resolver.rb +1 -1
- data/lib/lutaml/model/error/union_schema_unsupported_error.rb +11 -0
- data/lib/lutaml/model/global_context.rb +11 -8
- data/lib/lutaml/model/instrumentation.rb +2 -2
- data/lib/lutaml/model/mapping/mapping_rule.rb +0 -6
- data/lib/lutaml/model/register.rb +2 -2
- data/lib/lutaml/model/runtime_compatibility.rb +50 -0
- data/lib/lutaml/model/schema/class_loader.rb +58 -0
- data/lib/lutaml/model/schema/compiled_output.rb +83 -0
- data/lib/lutaml/model/schema/definitions/attribute.rb +32 -0
- data/lib/lutaml/model/schema/definitions/choice.rb +20 -0
- data/lib/lutaml/model/schema/definitions/facet.rb +40 -0
- data/lib/lutaml/model/schema/definitions/group_import.rb +22 -0
- data/lib/lutaml/model/schema/definitions/member_walk.rb +33 -0
- data/lib/lutaml/model/schema/definitions/model.rb +40 -0
- data/lib/lutaml/model/schema/definitions/namespace.rb +23 -0
- data/lib/lutaml/model/schema/definitions/restricted_type.rb +30 -0
- data/lib/lutaml/model/schema/definitions/sequence.rb +19 -0
- data/lib/lutaml/model/schema/definitions/simple_content.rb +22 -0
- data/lib/lutaml/model/schema/definitions/transform_facet.rb +21 -0
- data/lib/lutaml/model/schema/definitions/type_ref.rb +20 -0
- data/lib/lutaml/model/schema/definitions/union_type.rb +28 -0
- data/lib/lutaml/model/schema/definitions/xml_root.rb +20 -0
- data/lib/lutaml/model/schema/definitions.rb +24 -0
- data/lib/lutaml/model/schema/file_writer.rb +70 -0
- data/lib/lutaml/model/schema/generator/definitions_collection.rb +16 -2
- data/lib/lutaml/model/schema/generator/property.rb +21 -4
- data/lib/lutaml/model/schema/module_nesting.rb +31 -0
- data/lib/lutaml/model/schema/namespace_naming.rb +46 -0
- data/lib/lutaml/model/schema/registry_generator.rb +115 -0
- data/lib/lutaml/model/schema/renderers/base.rb +40 -0
- data/lib/lutaml/model/schema/renderers/mappings.rb +72 -0
- data/lib/lutaml/model/schema/renderers/member_decls.rb +115 -0
- data/lib/lutaml/model/schema/renderers/model.rb +121 -0
- data/lib/lutaml/model/schema/renderers/namespace.rb +30 -0
- data/lib/lutaml/model/schema/renderers/registration.rb +64 -0
- data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +93 -0
- data/lib/lutaml/model/schema/renderers/restricted_type.rb +88 -0
- data/lib/lutaml/model/schema/renderers/union.rb +83 -0
- data/lib/lutaml/model/schema/renderers.rb +19 -0
- data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +47 -0
- data/lib/lutaml/model/schema/rnc_compiler.rb +62 -0
- data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +91 -0
- data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +392 -0
- data/lib/lutaml/model/schema/rng_compiler/member_collector.rb +30 -0
- data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +152 -0
- data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +125 -0
- data/lib/lutaml/model/schema/rng_compiler.rb +220 -0
- data/lib/lutaml/model/schema/templates.rb +162 -0
- data/lib/lutaml/model/schema/xml_compiler/element_order.rb +41 -0
- data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +31 -107
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +191 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +216 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +153 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +167 -0
- data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +87 -0
- data/lib/lutaml/model/schema/xml_compiler.rb +38 -517
- data/lib/lutaml/model/schema.rb +19 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +23 -11
- data/lib/lutaml/model/serialize/builder.rb +70 -2
- data/lib/lutaml/model/serialize/initialization.rb +3 -2
- data/lib/lutaml/model/serialize/value_mapping.rb +9 -57
- data/lib/lutaml/model/serialize.rb +6 -9
- data/lib/lutaml/model/services/rule_value_extractor.rb +2 -3
- data/lib/lutaml/model/services.rb +0 -2
- data/lib/lutaml/model/store.rb +2 -1
- data/lib/lutaml/model/toml.rb +1 -1
- data/lib/lutaml/model/transform.rb +17 -39
- data/lib/lutaml/model/transformation_registry.rb +1 -1
- data/lib/lutaml/model/type.rb +1 -0
- data/lib/lutaml/model/union.rb +320 -0
- data/lib/lutaml/model/utils.rb +6 -0
- data/lib/lutaml/model/validation.rb +1 -4
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +18 -0
- data/lib/lutaml/toml.rb +1 -1
- data/lib/lutaml/xml/adapter.rb +1 -1
- data/lib/lutaml/xml/adapter_loader.rb +1 -1
- data/lib/lutaml/xml/builder/base.rb +1 -1
- data/lib/lutaml/xml/builder.rb +1 -1
- data/lib/lutaml/xml/error/schema_validation_error.rb +25 -0
- data/lib/lutaml/xml/model_transform.rb +5 -1
- data/lib/lutaml/xml/schema/relaxng_schema.rb +5 -0
- data/lib/lutaml/xml/schema/xsd_schema.rb +4 -0
- data/lib/lutaml/xml/serialization/format_conversion.rb +48 -0
- data/lib/lutaml/xml/serialization/instance_methods.rb +13 -0
- data/lib/lutaml/xml/transformation/element_builder.rb +21 -8
- data/lib/lutaml/xml/transformation/ordered_applier.rb +104 -4
- data/lib/lutaml/xml/xsd_validator.rb +66 -0
- data/lib/lutaml/xml.rb +53 -12
- data/lutaml-model.gemspec +4 -2
- data/spec/fixtures/xml/schema/rnc/address_book.rnc +15 -0
- data/spec/fixtures/xml/schema/rnc/book_features.rnc +18 -0
- data/spec/fixtures/xml/schema/rnc/includes/book.rnc +6 -0
- data/spec/fixtures/xml/schema/rnc/includes/circular_a.rnc +8 -0
- data/spec/fixtures/xml/schema/rnc/includes/circular_b.rnc +8 -0
- data/spec/fixtures/xml/schema/rnc/includes/library.rnc +8 -0
- data/spec/fixtures/xml/schema/rng/address_book.rng +16 -0
- data/spec/fixtures/xml/schema/rng/book.rng +27 -0
- data/spec/fixtures/xml/schema/rng/fixed_value.rng +11 -0
- data/spec/fixtures/xml/schema/rng/fragment_in_choice.rng +30 -0
- data/spec/fixtures/xml/schema/rng/full_name.rng +39 -0
- data/spec/fixtures/xml/schema/rng/integer_range.rng +32 -0
- data/spec/fixtures/xml/schema/rng/list_type.rng +12 -0
- data/spec/fixtures/xml/schema/rng/namespaced.rng +9 -0
- data/spec/fixtures/xml/schema/rng/paragraph.rng +13 -0
- data/spec/fixtures/xml/schema/rng/person.rng +23 -0
- data/spec/fixtures/xml/schema/rng/union.rng +14 -0
- data/spec/fixtures/xml/validate_xml_with_address.xsd +13 -0
- data/spec/fixtures/xml/validate_xml_with_contact.xsd +11 -0
- data/spec/fixtures/xml/validate_xml_with_person.xsd +11 -0
- data/spec/fixtures/xml/validate_xml_with_person_strict.xsd +17 -0
- data/spec/fixtures/xml/validate_xml_with_product.xsd +37 -0
- data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
- data/spec/lutaml/model/attribute_spec.rb +169 -12
- data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
- data/spec/lutaml/model/collection_spec.rb +18 -0
- data/spec/lutaml/model/mixed_content_spec.rb +171 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
- data/spec/lutaml/model/ordered_content_spec.rb +41 -0
- data/spec/lutaml/model/raw_element_spec.rb +125 -0
- data/spec/lutaml/model/rule_value_extractor_spec.rb +7 -14
- data/spec/lutaml/model/runtime_compatibility_spec.rb +25 -0
- data/spec/lutaml/model/schema/renderers/model_spec.rb +149 -0
- data/spec/lutaml/model/schema/renderers/namespace_spec.rb +36 -0
- data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +75 -0
- data/spec/lutaml/model/schema/renderers/union_spec.rb +58 -0
- data/spec/lutaml/model/serialize/builder_spec.rb +358 -0
- data/spec/lutaml/model/transform_apply_value_map_spec.rb +64 -0
- data/spec/lutaml/model/union_attribute_spec.rb +658 -0
- data/spec/lutaml/model/union_spec.rb +257 -0
- data/spec/lutaml/model/utils_spec.rb +14 -0
- data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
- data/spec/lutaml/xml/schema/compiler_spec.rb +21 -4
- data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +404 -0
- data/spec/lutaml/xml/schema/rng_compiler_spec.rb +893 -0
- data/spec/lutaml/xml/validate_xml_with_constructs_spec.rb +222 -0
- data/spec/lutaml/xml/validate_xml_with_spec.rb +186 -0
- data/spec/spec_helper.rb +28 -11
- data/spec/support/opal.rb +5 -2
- metadata +99 -31
- data/lib/lutaml/model/schema/xml_compiler/attribute.rb +0 -106
- data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +0 -47
- data/lib/lutaml/model/schema/xml_compiler/choice.rb +0 -67
- data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +0 -27
- data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +0 -34
- data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +0 -173
- data/lib/lutaml/model/schema/xml_compiler/element.rb +0 -121
- data/lib/lutaml/model/schema/xml_compiler/group.rb +0 -113
- data/lib/lutaml/model/schema/xml_compiler/restriction.rb +0 -104
- data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -52
- data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +0 -40
- data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +0 -265
- data/lib/lutaml/model/schema/xml_compiler/xml_namespace_class.rb +0 -110
- data/lib/lutaml/model/services/default_value_resolver.rb +0 -60
- data/spec/lutaml/model/services/default_value_resolver_spec.rb +0 -162
- data/spec/lutaml/xml/schema/compiler/attribute_group_spec.rb +0 -71
- data/spec/lutaml/xml/schema/compiler/attribute_spec.rb +0 -67
- data/spec/lutaml/xml/schema/compiler/choice_spec.rb +0 -74
- data/spec/lutaml/xml/schema/compiler/complex_content_restriction_spec.rb +0 -60
- data/spec/lutaml/xml/schema/compiler/complex_content_spec.rb +0 -41
- data/spec/lutaml/xml/schema/compiler/complex_type_spec.rb +0 -202
- data/spec/lutaml/xml/schema/compiler/element_spec.rb +0 -68
- data/spec/lutaml/xml/schema/compiler/group_spec.rb +0 -90
- data/spec/lutaml/xml/schema/compiler/restriction_spec.rb +0 -77
- data/spec/lutaml/xml/schema/compiler/sequence_spec.rb +0 -65
- data/spec/lutaml/xml/schema/compiler/simple_content_spec.rb +0 -61
- 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
|
-
|
|
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
|
-
|
|
15
|
+
begin
|
|
16
|
+
require "opal/rspec/rake_task"
|
|
9
17
|
|
|
10
|
-
|
|
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]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Builder DSL
|
|
3
|
+
nav_order: 99
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
= Builder DSL
|
|
7
|
+
:toc:
|
|
8
|
+
:toclevels: 3
|
|
9
|
+
|
|
10
|
+
The Builder DSL gives any `Lutaml::Model::Serializable` class a block-based
|
|
11
|
+
construction syntax:
|
|
12
|
+
|
|
13
|
+
[source,ruby]
|
|
14
|
+
----
|
|
15
|
+
person = Person.new do |p|
|
|
16
|
+
p.name "Ada"
|
|
17
|
+
p.email "ada@example.com"
|
|
18
|
+
end
|
|
19
|
+
----
|
|
20
|
+
|
|
21
|
+
It works for every model. For models declared with `ordered` or
|
|
22
|
+
`mixed_content`, the builder also records the order in which attributes were
|
|
23
|
+
mutated so that `to_xml` can emit child elements in the same order.
|
|
24
|
+
|
|
25
|
+
== Two equivalent syntaxes
|
|
26
|
+
|
|
27
|
+
Every attribute supports both the *appender* form and the *direct setter* form
|
|
28
|
+
inside the block. They are interchangeable:
|
|
29
|
+
|
|
30
|
+
[source,ruby]
|
|
31
|
+
----
|
|
32
|
+
# Appender form: looks like a method call with the value as argument
|
|
33
|
+
person = Person.new do |p|
|
|
34
|
+
p.name "Ada" # equivalent to p.name = "Ada"
|
|
35
|
+
p.email "ada@x.org" # equivalent to p.email = "ada@x.org"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Direct setter form: looks like a normal assignment
|
|
39
|
+
person = Person.new do |p|
|
|
40
|
+
p.name = "Ada"
|
|
41
|
+
p.email = "ada@x.org"
|
|
42
|
+
end
|
|
43
|
+
----
|
|
44
|
+
|
|
45
|
+
Both snippets produce identical `to_xml`, `to_json`, etc. Use whichever reads
|
|
46
|
+
better in context.
|
|
47
|
+
|
|
48
|
+
=== Collections
|
|
49
|
+
|
|
50
|
+
Collection attributes accept both forms too:
|
|
51
|
+
|
|
52
|
+
[source,ruby]
|
|
53
|
+
----
|
|
54
|
+
group = Group.new do |g|
|
|
55
|
+
# Append one item at a time:
|
|
56
|
+
g.member(person1)
|
|
57
|
+
g.member(person2)
|
|
58
|
+
|
|
59
|
+
# Or assign the whole collection at once:
|
|
60
|
+
g.members = [person1, person2, person3]
|
|
61
|
+
end
|
|
62
|
+
----
|
|
63
|
+
|
|
64
|
+
The wholesale assignment records one order-entry per item, so the resulting
|
|
65
|
+
`element_order` length matches the number of serialized child elements.
|
|
66
|
+
|
|
67
|
+
== Order tracking and serialization
|
|
68
|
+
|
|
69
|
+
The builder tracks call order only when *all* of the following are true:
|
|
70
|
+
|
|
71
|
+
1. The instance was constructed via `Klass.new do |x| ... end` (a block was
|
|
72
|
+
passed).
|
|
73
|
+
2. The XML mapping is declared with `ordered` or `mixed_content`.
|
|
74
|
+
|
|
75
|
+
When tracking is active, every mutation (via either syntax) is appended to the
|
|
76
|
+
instance's `element_order`. `to_xml` then iterates `element_order` to emit
|
|
77
|
+
child elements in the order they were set.
|
|
78
|
+
|
|
79
|
+
When tracking is *not* active (no block, or the model is neither `ordered` nor
|
|
80
|
+
`mixed_content`), `element_order` stays `nil` and `to_xml` emits elements in
|
|
81
|
+
declaration order.
|
|
82
|
+
|
|
83
|
+
=== Equivalence with `.tap`
|
|
84
|
+
|
|
85
|
+
The block form is the only way to enable tracking. Constructing the instance
|
|
86
|
+
separately and mutating it afterwards does not enable tracking:
|
|
87
|
+
|
|
88
|
+
[source,ruby]
|
|
89
|
+
----
|
|
90
|
+
# Tracking active:
|
|
91
|
+
item = Item.new { |i| i.b = "first"; i.a = "second" }
|
|
92
|
+
item.element_order.map(&:name) # => ["b", "a"]
|
|
93
|
+
|
|
94
|
+
# Tracking NOT active:
|
|
95
|
+
item = Item.new
|
|
96
|
+
item.b = "first"
|
|
97
|
+
item.a = "second"
|
|
98
|
+
item.element_order # => nil
|
|
99
|
+
# Falls back to declaration order for serialization.
|
|
100
|
+
----
|
|
101
|
+
|
|
102
|
+
If you want call-order preservation, use the block form.
|
|
103
|
+
|
|
104
|
+
== No silent data loss (invariant)
|
|
105
|
+
|
|
106
|
+
The serializer guarantees that every attribute with a non-default value appears
|
|
107
|
+
in the serialized output, regardless of which syntax was used to set it.
|
|
108
|
+
|
|
109
|
+
This is enforced by `OrderedApplier#apply_remaining_rules`: any element-typed
|
|
110
|
+
attribute not represented in `element_order` is emitted in declaration order
|
|
111
|
+
after the tracked entries. This is a safety net on top of the builder's own
|
|
112
|
+
tracking, so that future code paths (new mutation helpers, attribute merge
|
|
113
|
+
operations, etc.) cannot silently drop user data.
|
|
114
|
+
|
|
115
|
+
If you find an attribute missing from `to_xml`, the right answer is to fix the
|
|
116
|
+
mutation path so it records into `element_order`. The safety net guarantees
|
|
117
|
+
correctness; it does not guarantee order.
|
|
118
|
+
|
|
119
|
+
== Performance notes
|
|
120
|
+
|
|
121
|
+
For models that are neither `ordered` nor `mixed_content`, the builder does
|
|
122
|
+
not allocate any `Lutaml::Xml::Element` order-tracking objects. The check is
|
|
123
|
+
`@__order_tracking__` (a boolean), and the recording helpers are no-ops.
|
|
124
|
+
|
|
125
|
+
The check is also a no-op for `ordered`/`mixed_content` models constructed
|
|
126
|
+
without a block. This keeps the common case (`Klass.from_xml(xml)` followed
|
|
127
|
+
by `to_xml`) free of tracking overhead.
|
|
@@ -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
|