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.
- 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/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 +14 -2
- data/lib/lutaml/model/serialize/builder.rb +15 -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 +6 -0
- 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 +30 -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/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 +97 -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]
|
|
@@ -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
|
data/docs/_pages/validation.adoc
CHANGED
|
@@ -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
|
|