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
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Type
|
|
6
|
+
# Functional xsd:union: an attribute value conforms to one of several
|
|
7
|
+
# declared member types. A single shared class recognized by identity
|
|
8
|
+
# (mirrors Type::Reference); the per-attribute member list lives in the
|
|
9
|
+
# attribute's @options[:union_member_types]. Stateless: serialization is
|
|
10
|
+
# driven by value.class, never by per-instance tracking.
|
|
11
|
+
#
|
|
12
|
+
# This class owns ALL union knowledge: the conformance engine, the
|
|
13
|
+
# definition-time validation, and the per-format mapped-field source.
|
|
14
|
+
# Every serializer touchpoint is a thin identity check that delegates here.
|
|
15
|
+
class Union < Value
|
|
16
|
+
# Options that cannot be combined with a union member list.
|
|
17
|
+
INCOMPATIBLE_OPTIONS = %i[polymorphic raw with child_mappings].freeze
|
|
18
|
+
|
|
19
|
+
# Native Ruby class a parsed/plain-Ruby value already has when it
|
|
20
|
+
# conforms to a scalar member without needing a lexical-space parse.
|
|
21
|
+
NATIVE_CLASSES = {
|
|
22
|
+
Lutaml::Model::Type::Integer => ::Integer,
|
|
23
|
+
Lutaml::Model::Type::Float => ::Float,
|
|
24
|
+
Lutaml::Model::Type::Boolean => [::TrueClass, ::FalseClass],
|
|
25
|
+
Lutaml::Model::Type::String => ::String,
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# Native classes for value types whose dependency loads lazily
|
|
29
|
+
# (require "bigdecimal" / "date"). Looked up by constant name so the
|
|
30
|
+
# union never force-loads an optional dependency; if such a value
|
|
31
|
+
# exists at runtime, its constant is necessarily already defined.
|
|
32
|
+
# Decimal's native class (BigDecimal) loads lazily (require
|
|
33
|
+
# "bigdecimal"); looked up by constant name so the union never
|
|
34
|
+
# force-loads it. (Decimal is the only supported value type with a
|
|
35
|
+
# lazily-loaded native class.)
|
|
36
|
+
LAZY_NATIVE_CLASS_NAMES = {
|
|
37
|
+
Lutaml::Model::Type::Decimal => :BigDecimal,
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
INTEGER_LEXICAL = /\A\s*[+-]?\d+\s*\z/
|
|
41
|
+
# Matches Type::Boolean.cast (XSD true/false/1/0 plus the library's
|
|
42
|
+
# t/f/yes/no/y/n forms), so a union :boolean accepts the same inputs as
|
|
43
|
+
# a plain :boolean attribute.
|
|
44
|
+
BOOLEAN_LEXICAL = /\A(true|false|t|f|yes|no|y|n|1|0)\z/i
|
|
45
|
+
|
|
46
|
+
FLOAT_LEXICAL = lambda do |string|
|
|
47
|
+
Float(string).finite?
|
|
48
|
+
rescue ArgumentError, TypeError
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Whether a String input belongs to a scalar member's lexical space.
|
|
53
|
+
# Keyed by member class (identity), parallel to NATIVE_CLASSES.
|
|
54
|
+
LEXICAL_PREDICATES = {
|
|
55
|
+
Lutaml::Model::Type::Integer => ->(s) { s.match?(INTEGER_LEXICAL) },
|
|
56
|
+
Lutaml::Model::Type::Float => FLOAT_LEXICAL,
|
|
57
|
+
Lutaml::Model::Type::Decimal => FLOAT_LEXICAL,
|
|
58
|
+
Lutaml::Model::Type::Boolean => ->(s) { s.match?(BOOLEAN_LEXICAL) },
|
|
59
|
+
Lutaml::Model::Type::String => ->(_s) { true },
|
|
60
|
+
}.freeze
|
|
61
|
+
|
|
62
|
+
# Sound scalar union members are exactly the value types with a lexical
|
|
63
|
+
# predicate above: each maps to a distinct, stable Ruby runtime class
|
|
64
|
+
# (Integer, Float, BigDecimal, TrueClass/FalseClass, String), so the
|
|
65
|
+
# stateless value.class-driven serialization can always recover the
|
|
66
|
+
# member. Other value types are excluded because they cannot round-trip
|
|
67
|
+
# soundly in a union — e.g. Type::Date.cast can yield a DateTime, and
|
|
68
|
+
# Time / TimeWithoutDate share a Ruby Time class. Models are always OK.
|
|
69
|
+
SUPPORTED_VALUE_TYPES = LEXICAL_PREDICATES.keys.freeze
|
|
70
|
+
|
|
71
|
+
# Lossless numeric widening: a source value type may conform to a
|
|
72
|
+
# broader member. Narrowing (Float -> integer) and unrelated types are
|
|
73
|
+
# absent and therefore rejected. Exact-class matches are handled
|
|
74
|
+
# earlier by the instance_of? check in conform_scalar.
|
|
75
|
+
WIDENS_INTO = {
|
|
76
|
+
Integer => [Float, Decimal],
|
|
77
|
+
Float => [Decimal],
|
|
78
|
+
}.freeze
|
|
79
|
+
|
|
80
|
+
class << self
|
|
81
|
+
# Resolve the first member (in declared order) whose lexical/structural
|
|
82
|
+
# space the value belongs to, and the value cast to that member.
|
|
83
|
+
#
|
|
84
|
+
# @param value [Object] raw value (scalar, Hash sub-tree, or XmlElement)
|
|
85
|
+
# @param members [Array<Class>] resolved members, declared order
|
|
86
|
+
# @param format [Symbol, nil] serialization format being deserialized
|
|
87
|
+
# @param register [Symbol, nil] register for nested-model resolution
|
|
88
|
+
# @return [Array(Class, Object), nil] [member, casted] or nil (no match)
|
|
89
|
+
def conforming_member(value, members, format:, register:)
|
|
90
|
+
return nil if value.nil? || Utils.uninitialized?(value)
|
|
91
|
+
|
|
92
|
+
members.each do |member|
|
|
93
|
+
casted = if model_member?(member)
|
|
94
|
+
conform_model(member, value, format, register)
|
|
95
|
+
else
|
|
96
|
+
conform_scalar(member, value)
|
|
97
|
+
end
|
|
98
|
+
return [member, casted] unless casted == :no_match
|
|
99
|
+
end
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Whether a compiled mapping rule's attribute is a union — the single
|
|
104
|
+
# identity check every serializer touchpoint shares.
|
|
105
|
+
def rule?(rule)
|
|
106
|
+
rule.attribute_type == self
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# An XML child is routed to member resolution (vs the plain-text
|
|
110
|
+
# scalar path) when it has child elements or attributes. Resolution
|
|
111
|
+
# tries model members by key coverage, then falls back to the scalar
|
|
112
|
+
# members on the element's text — so a text-only element with an
|
|
113
|
+
# unrelated attribute (e.g. xml:lang) still resolves to a scalar.
|
|
114
|
+
def xml_structured?(element)
|
|
115
|
+
return false unless element.respond_to?(:children)
|
|
116
|
+
|
|
117
|
+
element.children.any?(&:element?) || element.attributes.any?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Serialize a scalar union value through the value type matching its
|
|
121
|
+
# own class, so output is canonical/portable (a BigDecimal emits
|
|
122
|
+
# "1.5", not a raw Ruby object). value.class drives the type —
|
|
123
|
+
# stateless, identical to how a plain scalar attribute serializes.
|
|
124
|
+
# Falls back to the value as-is when it maps to no known value type.
|
|
125
|
+
def serialize_scalar(value, format)
|
|
126
|
+
type = scalar_type_for(value)
|
|
127
|
+
return value unless type
|
|
128
|
+
|
|
129
|
+
type.new(value).public_send(:"to_#{format}")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def validate_members!(members)
|
|
133
|
+
unless members.is_a?(::Array) && !members.empty?
|
|
134
|
+
raise ArgumentError, "union type must be a non-empty array of types"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
resolved = members.map { |member| resolve_member!(member) }
|
|
138
|
+
validate_catch_all_position!(resolved)
|
|
139
|
+
resolved
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def validate_combo!(options)
|
|
143
|
+
present = INCOMPATIBLE_OPTIONS.select { |key| options[key] }
|
|
144
|
+
return if present.empty?
|
|
145
|
+
|
|
146
|
+
raise ArgumentError,
|
|
147
|
+
"union type cannot be combined with: #{present.join(', ')}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def model_member?(member)
|
|
153
|
+
member.is_a?(::Class) && member.include?(Serialize)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Structural (key-coverage) selection: every input key maps to one of
|
|
157
|
+
# the member's mapped fields, with no input key left unmapped. Once
|
|
158
|
+
# selected, field values cast leniently via the member's normal path.
|
|
159
|
+
# An existing member instance conforms as-is, keeping conformance
|
|
160
|
+
# idempotent — values can re-enter casting (e.g. a value transformer
|
|
161
|
+
# builds the member before the cast runs).
|
|
162
|
+
def conform_model(member, value, format, register)
|
|
163
|
+
return value if value.is_a?(member)
|
|
164
|
+
|
|
165
|
+
keys = input_keys(value, format)
|
|
166
|
+
return :no_match if keys.nil? || keys.empty?
|
|
167
|
+
|
|
168
|
+
field_names = member_field_names(member, format, register)
|
|
169
|
+
return :no_match unless keys.all? { |key| field_names.include?(key) }
|
|
170
|
+
|
|
171
|
+
# Plain-Ruby assignment (format nil) builds straight from the
|
|
172
|
+
# attribute hash, mirroring how non-union model attributes accept
|
|
173
|
+
# `Model.new(child: { ... })`; a format builds via the mappings.
|
|
174
|
+
return member.new(value) if format.nil?
|
|
175
|
+
|
|
176
|
+
member.apply_mappings(value, format, register: register)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# A scalar conforms if the value already is the member's native Ruby
|
|
180
|
+
# class (covers parsed and plain-Ruby values), or if a String input
|
|
181
|
+
# belongs to the member's space. Lenient built-ins (Integer/Float/
|
|
182
|
+
# Decimal/Boolean/String) are gated by an explicit lexical predicate
|
|
183
|
+
# so their forgiving casts can't false-match; every other value type
|
|
184
|
+
# owns its space via its own (strict) cast — nil/raise means no match.
|
|
185
|
+
def conform_scalar(member, value)
|
|
186
|
+
# Scalar payload: an XML element contributes its text (so a scalar
|
|
187
|
+
# member matches when no model member covers it); Hash and primitive
|
|
188
|
+
# inputs pass through unchanged.
|
|
189
|
+
value = value.text if xml_element?(value)
|
|
190
|
+
|
|
191
|
+
# Exact native class (instance_of?): the value is precisely the
|
|
192
|
+
# member's native Ruby class (covers parsed and plain-Ruby values).
|
|
193
|
+
native = Array(NATIVE_CLASSES[member] || lazy_native_class(member))
|
|
194
|
+
return value if native.any? { |klass| value.instance_of?(klass) }
|
|
195
|
+
|
|
196
|
+
predicate = LEXICAL_PREDICATES[member]
|
|
197
|
+
return :no_match unless predicate
|
|
198
|
+
|
|
199
|
+
# String input: accept only within the member's lexical space.
|
|
200
|
+
if value.is_a?(::String)
|
|
201
|
+
return predicate.call(value) ? member.cast(value) : :no_match
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Native non-string value (e.g. a parsed Integer for a :float
|
|
205
|
+
# member): accept only as a lossless widening into a broader member,
|
|
206
|
+
# so a lenient built-in can't narrow or distort it — :float accepts
|
|
207
|
+
# 42, but :integer won't swallow 3.7 or 3.0. lossless_cast then
|
|
208
|
+
# guards precision (e.g. 2**60+1 is not exactly representable as
|
|
209
|
+
# Float).
|
|
210
|
+
source = scalar_type_for(value)
|
|
211
|
+
return :no_match unless WIDENS_INTO[source]&.include?(member)
|
|
212
|
+
|
|
213
|
+
lossless_cast(member, value)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# A native non-string value conforms to a lenient built-in only if the
|
|
217
|
+
# member's cast preserves it (value-equal round-trip), so cross-numeric
|
|
218
|
+
# widening is accepted but narrowing/distortion is not.
|
|
219
|
+
def lossless_cast(member, value)
|
|
220
|
+
casted = member.cast(value)
|
|
221
|
+
casted == value ? casted : :no_match
|
|
222
|
+
rescue StandardError
|
|
223
|
+
:no_match
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def lazy_native_class(member)
|
|
227
|
+
const_name = LAZY_NATIVE_CLASS_NAMES[member]
|
|
228
|
+
return nil unless const_name && Object.const_defined?(const_name)
|
|
229
|
+
|
|
230
|
+
Object.const_get(const_name)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# The value type whose native Ruby class the value is an instance of
|
|
234
|
+
# (reverse of NATIVE_CLASSES plus the lazily-loaded native classes).
|
|
235
|
+
def scalar_type_for(value)
|
|
236
|
+
NATIVE_CLASSES.each do |type, klasses|
|
|
237
|
+
return type if Array(klasses).any? { |k| value.instance_of?(k) }
|
|
238
|
+
end
|
|
239
|
+
LAZY_NATIVE_CLASS_NAMES.each_key do |type|
|
|
240
|
+
klass = lazy_native_class(type)
|
|
241
|
+
return type if klass && value.instance_of?(klass)
|
|
242
|
+
end
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Input keys by format: Hash keys for key-value; child element local
|
|
247
|
+
# names + attribute names for XML; nil otherwise.
|
|
248
|
+
def input_keys(value, format)
|
|
249
|
+
return value.keys.map(&:to_s) if value.is_a?(::Hash)
|
|
250
|
+
|
|
251
|
+
if format == :xml && xml_element?(value)
|
|
252
|
+
names = value.children.select(&:element?).map(&:unprefixed_name) +
|
|
253
|
+
value.attributes.values.map(&:unprefixed_name)
|
|
254
|
+
return names.map(&:to_s)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
nil
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def xml_element?(value)
|
|
261
|
+
value.respond_to?(:children) && value.respond_to?(:attributes)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The set of mapped field names for a member in the given format.
|
|
265
|
+
# One source, parameterized by format. A nil format is plain-Ruby
|
|
266
|
+
# assignment, which has no mapping context — match on attribute names.
|
|
267
|
+
def member_field_names(member, format, register)
|
|
268
|
+
return member.attributes.keys.map(&:to_s) if format.nil?
|
|
269
|
+
|
|
270
|
+
mapping = member.mappings_for(format, register)
|
|
271
|
+
rules = if format == :xml
|
|
272
|
+
mapping.elements(register) + mapping.attributes(register)
|
|
273
|
+
else
|
|
274
|
+
mapping.mappings(register)
|
|
275
|
+
end
|
|
276
|
+
rules.flat_map do |rule|
|
|
277
|
+
(rule.multiple_mappings? ? rule.name : [rule.name]).map(&:to_s)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def resolve_member!(member)
|
|
282
|
+
if member.is_a?(::Hash)
|
|
283
|
+
raise ArgumentError,
|
|
284
|
+
"union members must be types or symbols; option hashes " \
|
|
285
|
+
"(e.g. { ref: ... }) are not supported as union members"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
klass =
|
|
289
|
+
begin
|
|
290
|
+
Lutaml::Model::Attribute.cast_type!(member)
|
|
291
|
+
rescue ArgumentError
|
|
292
|
+
nil
|
|
293
|
+
end
|
|
294
|
+
unless klass
|
|
295
|
+
raise ArgumentError, "invalid union member: #{member.inspect}"
|
|
296
|
+
end
|
|
297
|
+
return klass if klass.is_a?(::Class) && klass.include?(Serialize)
|
|
298
|
+
return klass if SUPPORTED_VALUE_TYPES.include?(klass)
|
|
299
|
+
|
|
300
|
+
raise ArgumentError,
|
|
301
|
+
"unsupported union member type: #{member.inspect}. Supported " \
|
|
302
|
+
"scalar members are :string, :integer, :float, :decimal, " \
|
|
303
|
+
":boolean, or a Serializable model"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# The catch-all must be the last member overall (not merely the last
|
|
307
|
+
# value type): XML scalar matching reads element.text, so a :string
|
|
308
|
+
# placed before a model member would swallow structured elements too.
|
|
309
|
+
def validate_catch_all_position!(resolved)
|
|
310
|
+
return unless resolved.include?(Lutaml::Model::Type::String)
|
|
311
|
+
return if resolved.last == Lutaml::Model::Type::String
|
|
312
|
+
|
|
313
|
+
raise ArgumentError,
|
|
314
|
+
"a universal catch-all type must be the last union member"
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
data/lib/lutaml/model/utils.rb
CHANGED
|
@@ -102,6 +102,12 @@ module Lutaml
|
|
|
102
102
|
snake_case(base_class_name(klass))
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
# Return the final segment of a `prefix:local` style qualified name,
|
|
106
|
+
# e.g. `"xs:string" -> "string"`. Returns nil for nil input.
|
|
107
|
+
def last_of_split(field)
|
|
108
|
+
field&.split(":")&.last
|
|
109
|
+
end
|
|
110
|
+
|
|
105
111
|
def initialized?(value)
|
|
106
112
|
!value.equal?(UNINITIALIZED)
|
|
107
113
|
end
|
|
@@ -14,10 +14,7 @@ module Lutaml
|
|
|
14
14
|
sub_errors = value.validate
|
|
15
15
|
errors.concat(sub_errors) if sub_errors.is_a?(Array)
|
|
16
16
|
else
|
|
17
|
-
|
|
18
|
-
attr, register, self
|
|
19
|
-
)
|
|
20
|
-
attr.validate_value!(value, register, resolver)
|
|
17
|
+
attr.validate_value!(value, register, instance_object: self)
|
|
21
18
|
end
|
|
22
19
|
rescue Lutaml::Model::CollectionCountOutOfRangeError => e
|
|
23
20
|
errors << e unless attr.choice
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/model.rb
CHANGED
|
@@ -142,6 +142,8 @@ module Lutaml
|
|
|
142
142
|
autoload :UnknownTypeError, "#{__dir__}/model/error/unknown_type_error"
|
|
143
143
|
autoload :RequiredAttributeMissingError,
|
|
144
144
|
"#{__dir__}/model/error/required_attribute_missing_error"
|
|
145
|
+
autoload :UnionSchemaUnsupportedError,
|
|
146
|
+
"#{__dir__}/model/error/union_schema_unsupported_error"
|
|
145
147
|
autoload :MultipleMappingsError,
|
|
146
148
|
"#{__dir__}/model/error/multiple_mappings_error"
|
|
147
149
|
autoload :CollectionTrueMissingError,
|
|
@@ -235,6 +237,22 @@ module Lutaml
|
|
|
235
237
|
def self.reset_configuration!
|
|
236
238
|
@configuration = nil
|
|
237
239
|
end
|
|
240
|
+
|
|
241
|
+
# Convenience accessors for runtime/platform detection.
|
|
242
|
+
# Delegates to RuntimeCompatibility so call sites can write
|
|
243
|
+
# `Lutaml::Model.opal?` (or `Model.opal?` inside the namespace)
|
|
244
|
+
# instead of the long `Lutaml::Model::RuntimeCompatibility.opal?`.
|
|
245
|
+
def self.opal?
|
|
246
|
+
RuntimeCompatibility.opal?
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def self.native?
|
|
250
|
+
RuntimeCompatibility.native?
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def self.windows?
|
|
254
|
+
RuntimeCompatibility.windows?
|
|
255
|
+
end
|
|
238
256
|
end
|
|
239
257
|
end
|
|
240
258
|
|
data/lib/lutaml/toml.rb
CHANGED
|
@@ -30,7 +30,7 @@ Lutaml::Model::FormatRegistry.register(
|
|
|
30
30
|
adapter_class: nil,
|
|
31
31
|
transformer: Lutaml::Toml::Adapter::Transform,
|
|
32
32
|
key_value: true,
|
|
33
|
-
adapter_options: if Lutaml::Model
|
|
33
|
+
adapter_options: if Lutaml::Model.opal?
|
|
34
34
|
nil
|
|
35
35
|
else
|
|
36
36
|
{
|
data/lib/lutaml/xml/adapter.rb
CHANGED
|
@@ -13,11 +13,11 @@ module Lutaml
|
|
|
13
13
|
autoload :NamespaceUriCollector,
|
|
14
14
|
"#{__dir__}/adapter/namespace_uri_collector"
|
|
15
15
|
autoload :OgaAdapter, "#{__dir__}/adapter/oga_adapter"
|
|
16
|
+
autoload :RexmlAdapter, "#{__dir__}/adapter/rexml_adapter"
|
|
16
17
|
Lutaml::Model::RuntimeCompatibility.autoload_native(
|
|
17
18
|
self,
|
|
18
19
|
NokogiriAdapter: "#{__dir__}/adapter/nokogiri_adapter",
|
|
19
20
|
OxAdapter: "#{__dir__}/adapter/ox_adapter",
|
|
20
|
-
RexmlAdapter: "#{__dir__}/adapter/rexml_adapter",
|
|
21
21
|
)
|
|
22
22
|
end
|
|
23
23
|
end
|
|
@@ -13,7 +13,7 @@ module Lutaml
|
|
|
13
13
|
# @param _adapter [String] The adapter format name ("xml")
|
|
14
14
|
# @param type [String] The normalized type name (e.g., "nokogiri_adapter")
|
|
15
15
|
def self.load_adapter_file(_adapter, type)
|
|
16
|
-
adapter_path = if Lutaml::Model
|
|
16
|
+
adapter_path = if Lutaml::Model.opal?
|
|
17
17
|
"lutaml/xml/adapter/#{type}"
|
|
18
18
|
else
|
|
19
19
|
File.join(File.dirname(__FILE__), "adapter", type)
|
data/lib/lutaml/xml/builder.rb
CHANGED
|
@@ -6,11 +6,11 @@ module Lutaml
|
|
|
6
6
|
module Builder
|
|
7
7
|
autoload :Base, "#{__dir__}/builder/base"
|
|
8
8
|
autoload :Oga, "#{__dir__}/builder/oga"
|
|
9
|
+
autoload :Rexml, "#{__dir__}/builder/rexml"
|
|
9
10
|
Lutaml::Model::RuntimeCompatibility.autoload_native(
|
|
10
11
|
self,
|
|
11
12
|
Nokogiri: "#{__dir__}/builder/nokogiri",
|
|
12
13
|
Ox: "#{__dir__}/builder/ox",
|
|
13
|
-
Rexml: "#{__dir__}/builder/rexml",
|
|
14
14
|
)
|
|
15
15
|
end
|
|
16
16
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
module Error
|
|
6
|
+
# Collected when a model's generated XML does not conform to the
|
|
7
|
+
# XSD schema configured via the `validate_xml_with` macro.
|
|
8
|
+
#
|
|
9
|
+
# One instance represents one schema violation reported by the
|
|
10
|
+
# validating parser, so Lutaml::Model::ValidationError#error_messages
|
|
11
|
+
# lists each violation individually.
|
|
12
|
+
#
|
|
13
|
+
# Not to be confused with Schema::Xsd::SchemaValidationError, which
|
|
14
|
+
# reports problems in an XSD document itself.
|
|
15
|
+
class SchemaValidationError < XmlError
|
|
16
|
+
attr_reader :schema_path
|
|
17
|
+
|
|
18
|
+
def initialize(message, schema_path)
|
|
19
|
+
@schema_path = schema_path
|
|
20
|
+
super("XML does not conform to schema #{schema_path}: #{message}")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -708,7 +708,9 @@ _effective_register)
|
|
|
708
708
|
base_cast_options[:resolved_type] = attr_type
|
|
709
709
|
|
|
710
710
|
children.each do |child|
|
|
711
|
-
if !rule_has_custom_method &&
|
|
711
|
+
if !rule_has_custom_method &&
|
|
712
|
+
(attr_type_is_serializable ||
|
|
713
|
+
(attr&.union? && ::Lutaml::Model::Type::Union.xml_structured?(child)))
|
|
712
714
|
# Performance: Build cast_options efficiently (dup + []= cheaper than merge)
|
|
713
715
|
cast_options = if (child_namespace_uri = child.namespace_uri)
|
|
714
716
|
ns_type = attr.type_with_namespace(effective_register,
|
|
@@ -984,6 +986,8 @@ effective_register = lutaml_register)
|
|
|
984
986
|
# @return [Boolean] true if a matching child likely exists
|
|
985
987
|
def child_matches_rule?(rule, child_names_set, default_namespace)
|
|
986
988
|
rule_names = rule.namespaced_names(default_namespace)
|
|
989
|
+
# Array#intersect? requires an Array argument; child_names_set may
|
|
990
|
+
# be a Set, so use the block form which works for any includable.
|
|
987
991
|
rule_names.any? { |rn| child_names_set.include?(rn) } ||
|
|
988
992
|
child_names_set.include?(rule.name.to_s)
|
|
989
993
|
end
|
|
@@ -31,6 +31,11 @@ module Lutaml
|
|
|
31
31
|
|
|
32
32
|
def self.generate_attributes(xml, klass, register)
|
|
33
33
|
klass.attributes.each do |name, attr|
|
|
34
|
+
if attr.union?
|
|
35
|
+
raise Lutaml::Model::UnionSchemaUnsupportedError.new(name,
|
|
36
|
+
"RelaxNG")
|
|
37
|
+
end
|
|
38
|
+
|
|
34
39
|
attr_type = attr.type(register)
|
|
35
40
|
if attr_type <= Lutaml::Model::Serialize
|
|
36
41
|
xml.ref(name: attr_type.name)
|
|
@@ -409,6 +409,10 @@ attr_name)
|
|
|
409
409
|
|
|
410
410
|
def self.get_attribute_xsd_type(attr, attr_type, register,
|
|
411
411
|
_mapping_rule = nil)
|
|
412
|
+
if attr.union?
|
|
413
|
+
raise Lutaml::Model::UnionSchemaUnsupportedError.new(attr.name, "XSD")
|
|
414
|
+
end
|
|
415
|
+
|
|
412
416
|
# 1. Check for deprecated attribute-level xsd_type override
|
|
413
417
|
return attr.options[:xsd_type] if attr.options[:xsd_type]
|
|
414
418
|
|
|
@@ -44,6 +44,50 @@ module Lutaml
|
|
|
44
44
|
@namespace_class&.prefix_default
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# Configure one or more XSD schemas that the model's generated
|
|
48
|
+
# XML must conform to. Checked during validate/validate!
|
|
49
|
+
# (issue #264). Repeated calls (including in subclasses) append.
|
|
50
|
+
#
|
|
51
|
+
# Relative paths resolve against the file that declares the macro,
|
|
52
|
+
# so a model works regardless of the process working directory.
|
|
53
|
+
#
|
|
54
|
+
# @param paths [Array<String, Pathname>] paths to XSD schema files
|
|
55
|
+
def validate_xml_with(*paths)
|
|
56
|
+
base = File.dirname(caller_locations(1, 1).first.path)
|
|
57
|
+
@own_xml_schema_paths = own_xml_schema_paths +
|
|
58
|
+
paths.flatten.map { |path| File.expand_path(path.to_s, base) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# All configured XSD schema paths, parent-first: inherited paths
|
|
62
|
+
# come before paths declared on this class.
|
|
63
|
+
#
|
|
64
|
+
# @return [Array<String>]
|
|
65
|
+
def xml_schema_paths
|
|
66
|
+
inherited = if superclass.respond_to?(:xml_schema_paths)
|
|
67
|
+
superclass.xml_schema_paths
|
|
68
|
+
else
|
|
69
|
+
[]
|
|
70
|
+
end
|
|
71
|
+
inherited + own_xml_schema_paths
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Validate a raw XML string against the configured schemas without
|
|
75
|
+
# instantiating a model — the explicit input-side counterpart to
|
|
76
|
+
# the validate/validate! output-side check.
|
|
77
|
+
#
|
|
78
|
+
# @param xml [String] an XML document
|
|
79
|
+
# @return [Array<Lutaml::Xml::Error::SchemaValidationError>]
|
|
80
|
+
def validate_xml(xml)
|
|
81
|
+
XsdValidator.validate(xml, xml_schema_paths)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @raise [Lutaml::Model::ValidationError] when the XML does not
|
|
85
|
+
# conform to every configured schema
|
|
86
|
+
def validate_xml!(xml)
|
|
87
|
+
errors = validate_xml(xml)
|
|
88
|
+
raise Lutaml::Model::ValidationError.new(errors) if errors.any?
|
|
89
|
+
end
|
|
90
|
+
|
|
47
91
|
# Override choice to set format: :xml so Choice knows which format it belongs to.
|
|
48
92
|
def choice(min: 1, max: 1, format: :xml, &)
|
|
49
93
|
super
|
|
@@ -204,6 +248,10 @@ module Lutaml
|
|
|
204
248
|
|
|
205
249
|
private
|
|
206
250
|
|
|
251
|
+
def own_xml_schema_paths
|
|
252
|
+
@own_xml_schema_paths ||= []
|
|
253
|
+
end
|
|
254
|
+
|
|
207
255
|
# Handle XML mapping class inheritance pattern (e.g., `xml SomeMapping`)
|
|
208
256
|
#
|
|
209
257
|
# When a mapping class is passed, inherit mappings from it directly.
|
|
@@ -263,6 +263,19 @@ module Lutaml
|
|
|
263
263
|
self.class.mappings_for(:xml, register)&.element_sequence
|
|
264
264
|
end
|
|
265
265
|
|
|
266
|
+
# Append XSD schema errors (validate_xml_with macro) to the
|
|
267
|
+
# standard validation errors. No-op unless schemas are configured.
|
|
268
|
+
#
|
|
269
|
+
# @see Lutaml::Model::Validation#validate
|
|
270
|
+
def validate(register: Lutaml::Model::Config.default_register)
|
|
271
|
+
errors = super
|
|
272
|
+
return errors if self.class.xml_schema_paths.empty?
|
|
273
|
+
|
|
274
|
+
errors.concat(
|
|
275
|
+
self.class.validate_xml(to_xml(register: register)),
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
|
|
266
279
|
private
|
|
267
280
|
|
|
268
281
|
# Build declaration plan from stored element reference (lazy mode).
|
|
@@ -28,13 +28,22 @@ register_id, register)
|
|
|
28
28
|
# class AND the value is not a nil/empty marker from collection handlers.
|
|
29
29
|
# Nil and empty string markers should create simple elements, not attempt
|
|
30
30
|
# nested model transformation.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
# Union: dispatch on the value's own class (stateless). A model member
|
|
32
|
+
# becomes a nested element; a scalar member a simple value element.
|
|
33
|
+
union = Lutaml::Model::Type::Union.rule?(rule)
|
|
34
|
+
|
|
35
|
+
is_nested_model = !value.nil? &&
|
|
36
|
+
!(value.is_a?(String) && value.empty?) &&
|
|
37
|
+
if union
|
|
38
|
+
value.is_a?(Lutaml::Model::Serialize)
|
|
39
|
+
else
|
|
40
|
+
rule.attribute_type.is_a?(Class) &&
|
|
41
|
+
rule.attribute_type < Lutaml::Model::Serialize
|
|
42
|
+
end
|
|
35
43
|
|
|
36
44
|
if is_nested_model
|
|
37
|
-
create_nested_model_element(rule, value, options, register
|
|
45
|
+
create_nested_model_element(rule, value, options, register,
|
|
46
|
+
union: union)
|
|
38
47
|
else
|
|
39
48
|
create_simple_value_element(rule, value, options, model_class,
|
|
40
49
|
register_id)
|
|
@@ -119,7 +128,8 @@ parent_element_form_default)
|
|
|
119
128
|
# @param options [Hash] Options
|
|
120
129
|
# @param register [Register, nil] The register
|
|
121
130
|
# @return [::Lutaml::Xml::DataModel::XmlElement] The created element
|
|
122
|
-
def create_nested_model_element(rule, value, options, register
|
|
131
|
+
def create_nested_model_element(rule, value, options, register,
|
|
132
|
+
union: false)
|
|
123
133
|
# Resolve polymorphic configuration
|
|
124
134
|
polymorphic_config = rule.options[:polymorphic]
|
|
125
135
|
is_polymorphic = polymorphic_config.is_a?(::Hash) ? !polymorphic_config.empty? : !!polymorphic_config
|
|
@@ -130,8 +140,11 @@ parent_element_form_default)
|
|
|
130
140
|
actual_class = resolve_polymorphic_class(rule, value, is_polymorphic,
|
|
131
141
|
is_polymorphic_subtype)
|
|
132
142
|
|
|
133
|
-
# Get transformation for the actual class
|
|
134
|
-
|
|
143
|
+
# Get transformation for the actual class. Unions resolve the member
|
|
144
|
+
# from the value's own class, like polymorphism.
|
|
145
|
+
child_transformation = if union
|
|
146
|
+
value.class.transformation_for(:xml, register)
|
|
147
|
+
elsif is_polymorphic || is_polymorphic_subtype
|
|
135
148
|
actual_class.transformation_for(:xml,
|
|
136
149
|
register)
|
|
137
150
|
else
|