Class: Lutaml::Model::Type::Union
- Defined in:
- lib/lutaml/model/union.rb
Overview
Functional xsd:union: an attribute value conforms to one of several declared member types. A single shared class recognized by identity (mirrors Type::Reference); the per-attribute member list lives in the attribute's @options. Stateless: serialization is driven by value.class, never by per-instance tracking.
This class owns ALL union knowledge: the conformance engine, the definition-time validation, and the per-format mapped-field source. Every serializer touchpoint is a thin identity check that delegates here.
Constant Summary collapse
- INCOMPATIBLE_OPTIONS =
Options that cannot be combined with a union member list.
%i[polymorphic raw with child_mappings].freeze
- NATIVE_CLASSES =
Native Ruby class a parsed/plain-Ruby value already has when it conforms to a scalar member without needing a lexical-space parse.
{ Lutaml::Model::Type::Integer => ::Integer, Lutaml::Model::Type::Float => ::Float, Lutaml::Model::Type::Boolean => [::TrueClass, ::FalseClass], Lutaml::Model::Type::String => ::String, }.freeze
- LAZY_NATIVE_CLASS_NAMES =
Native classes for value types whose dependency loads lazily (require "bigdecimal" / "date"). Looked up by constant name so the union never force-loads an optional dependency; if such a value exists at runtime, its constant is necessarily already defined. Decimal's native class (BigDecimal) loads lazily (require "bigdecimal"); looked up by constant name so the union never force-loads it. (Decimal is the only supported value type with a lazily-loaded native class.)
{ Lutaml::Model::Type::Decimal => :BigDecimal, }.freeze
- INTEGER_LEXICAL =
/\A\s*[+-]?\d+\s*\z/- BOOLEAN_LEXICAL =
Matches Type::Boolean.cast (XSD true/false/1/0 plus the library's t/f/yes/no/y/n forms), so a union :boolean accepts the same inputs as a plain :boolean attribute.
/\A(true|false|t|f|yes|no|y|n|1|0)\z/i- FLOAT_LEXICAL =
lambda do |string| Float(string).finite? rescue ArgumentError, TypeError false end
- LEXICAL_PREDICATES =
Whether a String input belongs to a scalar member's lexical space. Keyed by member class (identity), parallel to NATIVE_CLASSES.
{ Lutaml::Model::Type::Integer => ->(s) { s.match?(INTEGER_LEXICAL) }, Lutaml::Model::Type::Float => FLOAT_LEXICAL, Lutaml::Model::Type::Decimal => FLOAT_LEXICAL, Lutaml::Model::Type::Boolean => ->(s) { s.match?(BOOLEAN_LEXICAL) }, Lutaml::Model::Type::String => ->(_s) { true }, }.freeze
- SUPPORTED_VALUE_TYPES =
Sound scalar union members are exactly the value types with a lexical predicate above: each maps to a distinct, stable Ruby runtime class (Integer, Float, BigDecimal, TrueClass/FalseClass, String), so the stateless value.class-driven serialization can always recover the member. Other value types are excluded because they cannot round-trip soundly in a union — e.g. Type::Date.cast can yield a DateTime, and Time / TimeWithoutDate share a Ruby Time class. Models are always OK.
LEXICAL_PREDICATES.keys.freeze
- WIDENS_INTO =
Lossless numeric widening: a source value type may conform to a broader member. Narrowing (Float -> integer) and unrelated types are absent and therefore rejected. Exact-class matches are handled earlier by the instance_of? check in conform_scalar.
{ Integer => [Float, Decimal], Float => [Decimal], }.freeze
Constants inherited from Value
Instance Attribute Summary
Attributes inherited from Value
Class Method Summary collapse
-
.conforming_member(value, members, format:, register:) ⇒ Array(Class, Object)?
Resolve the first member (in declared order) whose lexical/structural space the value belongs to, and the value cast to that member.
-
.rule?(rule) ⇒ Boolean
Whether a compiled mapping rule's attribute is a union — the single identity check every serializer touchpoint shares.
-
.serialize_scalar(value, format) ⇒ Object
Serialize a scalar union value through the value type matching its own class, so output is canonical/portable (a BigDecimal emits "1.5", not a raw Ruby object).
- .validate_combo!(options) ⇒ Object
- .validate_members!(members) ⇒ Object
-
.xml_structured?(element) ⇒ Boolean
An XML child is routed to member resolution (vs the plain-text scalar path) when it has child elements or attributes.
Methods inherited from Value
cast, format_type_serializer_for, from_format, #initialize, #initialized?, register_format_to_from_methods, register_format_type_serializer, serialize, #to_s
Methods included from Xml::Type::Configurable
Methods included from UninitializedClassGuard
Constructor Details
This class inherits a constructor from Lutaml::Model::Type::Value
Class Method Details
.conforming_member(value, members, format:, register:) ⇒ Array(Class, Object)?
Resolve the first member (in declared order) whose lexical/structural space the value belongs to, and the value cast to that member.
89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/lutaml/model/union.rb', line 89 def conforming_member(value, members, format:, register:) return nil if value.nil? || Utils.uninitialized?(value) members.each do |member| casted = if model_member?(member) conform_model(member, value, format, register) else conform_scalar(member, value) end return [member, casted] unless casted == :no_match end nil end |
.rule?(rule) ⇒ Boolean
Whether a compiled mapping rule's attribute is a union — the single identity check every serializer touchpoint shares.
105 106 107 |
# File 'lib/lutaml/model/union.rb', line 105 def rule?(rule) rule.attribute_type == self end |
.serialize_scalar(value, format) ⇒ Object
Serialize a scalar union value through the value type matching its own class, so output is canonical/portable (a BigDecimal emits "1.5", not a raw Ruby object). value.class drives the type — stateless, identical to how a plain scalar attribute serializes. Falls back to the value as-is when it maps to no known value type.
125 126 127 128 129 130 |
# File 'lib/lutaml/model/union.rb', line 125 def serialize_scalar(value, format) type = scalar_type_for(value) return value unless type type.new(value).public_send(:"to_#{format}") end |
.validate_combo!(options) ⇒ Object
142 143 144 145 146 147 148 |
# File 'lib/lutaml/model/union.rb', line 142 def validate_combo!() present = INCOMPATIBLE_OPTIONS.select { |key| [key] } return if present.empty? raise ArgumentError, "union type cannot be combined with: #{present.join(', ')}" end |
.validate_members!(members) ⇒ Object
132 133 134 135 136 137 138 139 140 |
# File 'lib/lutaml/model/union.rb', line 132 def validate_members!(members) unless members.is_a?(::Array) && !members.empty? raise ArgumentError, "union type must be a non-empty array of types" end resolved = members.map { |member| resolve_member!(member) } validate_catch_all_position!(resolved) resolved end |
.xml_structured?(element) ⇒ Boolean
An XML child is routed to member resolution (vs the plain-text scalar path) when it has child elements or attributes. Resolution tries model members by key coverage, then falls back to the scalar members on the element's text — so a text-only element with an unrelated attribute (e.g. xml:lang) still resolves to a scalar.
114 115 116 117 118 |
# File 'lib/lutaml/model/union.rb', line 114 def xml_structured?(element) return false unless element.respond_to?(:children) element.children.any?(&:element?) || element.attributes.any? end |