Class: Lutaml::Model::Type::Union

Inherits:
Value
  • Object
show all
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

Value::EMPTY_OPTIONS

Instance Attribute Summary

Attributes inherited from Value

#value

Class Method Summary collapse

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

included

Methods included from UninitializedClassGuard

#cast, #serialize

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.

Parameters:

  • value (Object)

    raw value (scalar, Hash sub-tree, or XmlElement)

  • members (Array<Class>)

    resolved members, declared order

  • format (Symbol, nil)

    serialization format being deserialized

  • register (Symbol, nil)

    register for nested-model resolution

Returns:

  • (Array(Class, Object), nil)

    [member, casted] or nil (no match)



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.

Returns:



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

Raises:

  • (ArgumentError)


142
143
144
145
146
147
148
# File 'lib/lutaml/model/union.rb', line 142

def validate_combo!(options)
  present = INCOMPATIBLE_OPTIONS.select { |key| options[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.

Returns:



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