Introduction
There are three types of registers in Lutaml::Model:
-
TypeRegister
-
ModelRegister
-
GlobalRegister
TypeRegister
The TypeRegister is a registry class that registers and looks up the Lutaml::Model::Type::Value classes only.
Register a Type::Value class
The following syntax registers a Type::Value class:
# assuming we have a `CustomString` class that inherits from Lutaml::Model::Type::Value
Lutaml::Model::Type.register(:custom_string, Lutaml::Model::Type::CustomString) TypeError is raised if the class does not inherit from Lutaml::Model::Type::Value. |
Lookup a Type::Value class
Lookup a Type::Value class using the assigned name:
Lutaml::Model::Type.lookup(:custom_string) # returns Lutaml::Model::Type::CustomString Lutaml::Model::Type::UnknownTypeError is raised if the name is not found in the registry. |
When looking up a class, the class is returned without looking up in the registry.
Lutaml::Model::Type.lookup(Lutaml::Model::Type::CustomString) # returns Lutaml::Model::Type::CustomString even if it's not registered in the registryLazy type registration with Ruby autoload
For large schema libraries with many model classes, you can defer loading until a type is actually needed by registering a Proc instead of a class. The Proc is resolved on first lookup, and Ruby’s autoload triggers the file load at that point.
# Declare autoload so Ruby loads the file when the constant is first referenced
autoload :Divergence, "mml/v3/vector_calculus"
# Register a Proc — the file is NOT loaded yet
Lutaml::Model::Type.register(:divergence, -> { Mml::V3::Divergence })
# The first lookup triggers autoload and resolves the class
Lutaml::Model::Type.lookup(:divergence)
# => Mml::V3::Divergence (file loaded now, Proc result cached)
# Subsequent lookups return the cached class directly
Lutaml::Model::Type.lookup(:divergence)
# => Mml::V3::Divergence (no Proc call)Why a Proc and not a bare Symbol? A Symbol (:Divergence) has no namespace context. A Proc captures the full constant path (Mml::V3::Divergence) and triggers autoload when called.
This pattern is especially useful in library entry points where you declare many types but only a subset is used at runtime. It pairs naturally with Ruby’s built-in autoload for lazy file loading.
If the Proc raises (e.g., NameError from a missing constant), the error propagates naturally and the registry is not mutated, so a retry is possible after the autoload path is configured.
ModelRegister
The ModelRegister is a registry class that registers and looks up the Lutaml::Model::Registrable classes (by default, Lutaml::Model::Serializable classes are Registrable classes). For consistency, the Lutaml::Model::Type::Value classes are registered similarly, but within the TypeRegister registry, as referenced in the previous section.
| Make sure to register the ModelRegister in GlobalRegister before using it. |
Register a Class
Register a Model class using the following syntax:
# assuming we have a `CustomModel` class that inherits from Lutaml::Model::Serializable
Lutaml::Model::Register.register_model(Lutaml::Model::CustomModel, id: :custom_model)This method register_model registers the class and assigns it the passed ID, which is :custom_model in this case. But if a model is registered without an ID, the class name is used as the ID. For example:
Lutaml::Model::Register.register_model(Lutaml::Model::AnotherCustomModel)This will register the class AnotherCustomModel with the ID :another_custom_model.
Register model tree
The register_model_tree method registers all the classes in the provided Model’s hierarchy. For example:
register = Lutaml::Model::Register.new(:v1)
module Mathml
class Mrow < Lutaml::Model::Serializable
attribute :mstyle, Mstyle
end
class Mstyle < Lutaml::Model::Serializable
attribute :mi, :string
end
class Math < Lutaml::Model::Serializable
attribute :mrow, Mrow
attribute :mstyle, Mstyle
end
end
register.register_model_tree(Mathml::Math) # registers all the classes in the Mathml::Math model tree, in this case Mathml::Mstyle and Mathml::MrowLookup a Class
Lookup a Model class using the assigned name:
register = Lutaml::Model::Register.new(:v1)
register.get_class(:custom_model) # returns Lutaml::Model::CustomModelThe class returned from the get_class method is also aware of the ModelRegister it was registered in. This is useful when you want to use the class directly. For example:
register = Lutaml::Model::Register.new(:v1)
json_hash = {
"mstyle": {
"mrow": {
"mi": "x",
"mo": "+"
}
},
"mrow": {
"mi": "z",
}
}
module Mathml
class Mrow < Lutaml::Model::Serializable
attribute :mi, :string
attribute :mo, :string
end
class Mstyle < Lutaml::Model::Serializable
attribute :mrow, Mrow
attribute :mi, :string
attribute :mo, :string
end
class Math < Lutaml::Model::Serializable
attribute :mrow, Mrow
attribute :mstyle, Mstyle
end
end
register.register_model_tree(Mathml::Math) # registers all the classes in the Mathml::Math model tree, in this case Mstyle and Mrow
# lookup the class and call the desired method, in current case from_json
register.get_class(:math).from_json(json_hash.to_json)
> #<Testing::Math:0x00000002ccd5a678
@mrow=#<Testing::Mrow:0x00000002cc50a1f8 @mi="z", @mo=nil>,
@mstyle=#<Testing::Mstyle:0x00000002cc50a108 @mi=nil, @mo=nil, @mrow=#<Testing::Mrow:0x00000002cc509fc8 @mi="x", @mo="+">>> If the class is not found in either the ModelRegister or the TypeRegister, a Lutaml::Model::UnknownTypeError is raised. |
Global Type substitution
The Lutaml::Model::Register class also provides a method to substitute a type globally. This is useful when you want to replace a type with another type in the entire model tree. For example:
register = Lutaml::Model::Register.new(:v1)
json_hash = {
"mstyle": {
"mrow": {
"mi": "x",
"mo": "+"
}
},
"mrow": {
"mi": "z",
"mstyle": {
"mrow": {
"mi": "x",
"mo": "+"
}
}
}
}
module Mathml
class String < Lutaml::Model::Type::Value
def to_json(*args)
"custom-string: #{super(*args).to_json}"
end
end
class Mrow < Lutaml::Model::Serializable
attribute :mi, :string
attribute :mo, :string
end
class Mstyle < Lutaml::Model::Serializable
attribute :mrow, Mrow
attribute :mi, :string
attribute :mo, :string
end
class Math < Lutaml::Model::Serializable
attribute :mrow, Mrow
attribute :mstyle, Mstyle
end
class ExtendedMrow < Mrow
attribute :mstyle, :mstyle
end
end
register.register_model_tree(Mathml::Math) # registers all the classes in the Mathml::Math model tree, in this case Mstyle and Mrow
# Substitute the Mrow class with the ExtendedMrow class globally
register.register_global_type_substitution(
from_type: Mathml::Mrow,
to_type: Mathml::ExtendedMrow
) # this will replace all instances of Mrow with ExtendedMrow in the entire model tree for this register
register.register_global_type_substitution(
from_type: Lutaml::Model::Type::String,
to_type: Mathml::String
)
# lookup the class and call the desired method, in current case from_json
models = register.get_class(:math).from_json(json_hash.to_json)
models.to_json
> "{\"mrow\":{\"mi\":\"custom-string: \\\"z\\\"\",\"mstyle\":{\"mrow\":{\"mi\":\"custom-string: \\\"x\\\"\",\"mo\":\"custom-string: \\\"+\\\"\"}}},\"mstyle\":{\"mrow\":{\"mi\":\"custom-string: \\\"x\\\"\",\"mo\":\"custom-string: \\\"+\\\"\"}}}"GlobalRegister
The GlobalRegister is a singleton that registers all the ModelRegisters. Model registers can be registered using the following syntax:
v1_register = Lutaml::Model::Register.new(:v1)
global_register = Lutaml::Model::GlobalRegister
global_register.register(v1_register) # register a Model register
# OR
global_register.instance.register(v1_register) # register a Model registerThe register method registers the ModelRegister based on its ID. The ID is used to look up the ModelRegister using the following syntax:
global_register.lookup(:v2) # fetch a Model register
# OR
global_register.instance.lookup(:v2) # fetch a Model registerIf a register is not needed anymore, it can be removed using the following syntax:
global_register.remove(:v1) # remove a ModelRegister using the its ID
# OR
global_register.instance.remove(:v1) # remove a ModelRegister using the it's IDRegister resolution fallback
General
Model registers support hierarchical fallback register resolution, which allows registers to depend on other registers for type resolution.
The following use cases benefit from this feature:
-
Shared types: Common types like
xs:annotation,xs:documentationcan resolved from parent registers without duplicating them in every register -
Isolation: Registers can disable fallback for strict schema isolation
-
Inheritance: Specialized schemas can extend base schemas by falling back to core registers
Default behavior
By default, all custom registers (except :default) automatically fall back to the :default (global) register.
This means if a type is not found in a custom register, it will be searched in the :default register.
register = Lutaml::Model::Register.new(:my_schema)
register.fallback # => [:default]The :default register itself has no fallback chain:
default_register = Lutaml::Model::GlobalRegister.lookup(:default)
default_register.fallback # => []Isolated registers
To create a register that does not fall back to any other register (complete isolation), pass an empty array as the fallback parameter:
isolated = Lutaml::Model::Register.new(:pristine, fallback: [])
isolated.fallback # => []Isolated registers will only resolve models explicitly registered within them.
This is useful for:
-
Testing schema isolation
-
Ensuring no dependency on external types
-
Strict schema validation
Explicit fallback chains
For complex schema hierarchies, you can specify an explicit fallback chain:
# Multi-level fallback: :profile → :core → :default
profile = Lutaml::Model::Register.new(
:gml_profile,
fallback: [:gml_core, :iso_types, :default]
)
profile.fallback # => [:gml_core, :iso_types, :default]Resolution order is by the order of the fallback array.
For example, when resolving a type in the :gml_profile register:
-
Search in local register (
:gml_profile) -
If not found, search in first fallback (
:gml_core) -
If not found, search in second fallback (
:iso_types) -
If not found, search in third fallback (
:default) -
If still not found, raise
UnknownTypeError
Use cases
Vertical separation (schema profiles)
Use fallback chains when specialized schemas extend base schemas:
# Base schema with core types
core_register = Lutaml::Model::Register.new(:gml_core)
Lutaml::Model::GlobalRegister.instance.register(core_register)
core_register.register_model(GeometryType, id: :geometry_type)
core_register.register_model(CoordinateType, id: :coordinate_type)
# Specialized profile extends core
profile_register = Lutaml::Model::Register.new(
:gml_profile,
fallback: [:gml_core, :default]
)
Lutaml::Model::GlobalRegister.instance.register(profile_register)
profile_register.register_model(ProfileGeometryType, id: :profile_geometry)
# Profile models can use both profile-specific and core types
class ProfileDocument < Lutaml::Model::Serializable
@register = profile_register
attribute :geometry, :profile_geometry # From :gml_profile
attribute :coordinate, :coordinate_type # From :gml_core (fallback)
endMulti-version schema support
Use fallback when newer schema versions build on older versions:
# Generate UnitsML schema with module namespace
Lutaml::Model::Schema::XmlCompiler.to_models(
xsd_content,
module_namespace: "UnitsMLV0919",
register_id: :unitsmlv0919, # Implicitly falls back to :default
output_dir: "lib/unitsml",
create_files: true
)
# Load models
require "unitsml/unitsmlv0919_registry"
UnitsMLV0919.register_all
# UnitsMLV0919 models resolve:
# 1. Domain-specific types from :unitsmlv0919 register
# 2. Common types (annotation, documentation) from :default register (fallback)
doc = UnitsMLV0919::UnitsMLType.from_xml(xml)Per-class default register
When using versioned schemas (e.g., MathML v2, v3, v4), each version may have its own register context. The lutaml_default_register class method allows a Serializable subclass to specify its preferred default register context.
The problem
Without lutaml_default_register, when you instantiate a class without an explicit register: option, it falls back to Config.default_register (:default):
# :mml_v2 context has different type mappings than :default
math = Mml::V2::Math.new # Falls back to :default register
# => UnknownTypeError because :default doesn't have MML typesYou must explicitly pass register: :mml_v2:
math = Mml::V2::Math.new({}, register: :mml_v2) # WorksThe solution
Define lutaml_default_register in a base class for your version:
module Mml
class V2Base < Lutaml::Model::Serializable
def self.lutaml_default_register
:mml_v2
end
end
class Math < V2Base
attribute :mrow, Mrow
xml do
element "math"
end
end
endNow Mml::Math.new automatically uses :mml_v2 as the default register:
math = Mml::Math.new # Uses :mml_v2 by default
math.to_xml # Works correctly with v2 typesResolution order
The register ID is resolved in this order:
-
Explicit
register:option - passed toneworfrom_xmletc. -
lutaml_default_register- class method returning Symbol or nil -
@registerinstance variable - set byregister_model_tree -
Config.default_register- global fallback (:default)
# Resolution examples for a class with lutaml_default_register = :mml_v2
math = Math.new # => :mml_v2 (from lutaml_default_register)
math = Math.new({}, register: :mml_v3) # => :mml_v3 (explicit option wins)
math = Math.new({}, register: nil) # => :mml_v2 (lutaml_default_register still used)
math = Math.new({}, register: false) # => :default (false is falsy)Using with versioned base classes
This pattern is especially useful for schema libraries with multiple versions:
module Mml
# Base class for v2
class V2Base < Lutaml::Model::Serializable
def self.lutaml_default_register
:mml_v2
end
end
# Base class for v3
class V3Base < Lutaml::Model::Serializable
def self.lutaml_default_register
:mml_v3
end
end
# V2-specific classes
class V2::Math < V2Base
# Uses :mml_v2 by default
end
class V2::Factorof < V2Base
# Uses :mml_v2 by default
end
# V3-specific classes
class V3::Math < V3Base
# Uses :mml_v3 by default
end
endEach version’s classes automatically use their correct register without requiring explicit register: options everywhere.
Thread safety
lutaml_default_register is a class method that returns a constant value, so it is thread-safe. Unlike modifying Config.default_register (which is global state), each class specifies its own default independently.
Instance-level register context during XML parsing
When parsing XML documents with nested elements, the parser must determine which register context to use for each element’s type resolution. The key insight is that when a Serializable model instance has a lutaml_register (set during instantiation), that instance-level register is used for all child element processing.
The problem
Consider a document that embeds MathML v2 elements:
class MyDocument < Lutaml::Model::Serializable
attribute :math, Mml::V2::Math # Has lutaml_default_register = :mml_v2
xml do
element "document"
map_element "math", to: :math
end
endWhen parsing:
<document>
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mmultiscripts><mi>x</mi></mmultiscripts>
</math>
</document>Without proper instance-level register context:
-
MyDocumentis in:defaultregister -
Parser creates
Mml::V2::Mathinstance withlutaml_register = :mml_v2(correct) -
When processing
<mmultiscripts>, the parser was using:default(parent’s register) -
:mmultiscriptsonly exists in:mml_v2→UnknownTypeError
The solution: effective_register
The XML parsing infrastructure uses an effective_register computed from the instance being populated, not the Transform object:
effective_register = if instance.is_a?(::Lutaml::Model::Serialize) &&
instance.respond_to?(:lutaml_register) &&
instance.lutaml_register
instance.lutaml_register
else
lutaml_register
endThis effective_register is then used for:
-
Mapping resolution (
mappings_for(:xml, effective_register)) -
Type resolution (
attr.type(effective_register)) -
Nested element casting (
attr.cast(child, :xml, effective_register, options)) -
Namespace-aware type resolution
Requirements
For this to work correctly:
-
The child class must have
lutaml_default_registerdeclared, OR -
The child instance must be created with the correct
lutaml_register
When both conditions are met, the instance’s lutaml_register takes precedence, ensuring child elements are resolved in the correct context.
# Versioned base class declares its default register
module Mml
class V2Base < Lutaml::Model::Serializable
def self.lutaml_default_register
:mml_v2
end
end
class Math < V2Base
attribute :mmultiscripts_value, Mmultiscripts, collection: true
end
end
# When parsing, the Math instance uses its lutaml_register (:mml_v2)
# for all child element resolution
doc = MyDocument.from_xml(xml)
doc.math.lutaml_register # => :mml_v2
doc.math.mmultiscripts_value.first.class # => Mml::V2::MmultiscriptsComplete example: embedding versioned schemas
# 1. Define versioned base class
module Mml
class V2Base < Lutaml::Model::Serializable
def self.lutaml_default_register
:mml_v2
end
end
class Mmultiscripts < V2Base
attribute :mi_value, :string
xml do
element "mmultiscripts"
map_element "mi", to: :mi_value
end
end
class Math < V2Base
attribute :mmultiscripts_value, Mmultiscripts, collection: true
xml do
element "math"
map_element "mmultiscripts", to: :mmultiscripts_value
end
end
end
# 2. Register in the versioned register
mml_v2_register = Lutaml::Model::Register.new(:mml_v2)
mml_v2_register.register_model_tree(Mml::Math)
Lutaml::Model::GlobalRegister.instance.register(mml_v2_register)
# 3. Use in a document with different register context
class MyDocument < Lutaml::Model::Serializable
attribute :id, :string
attribute :math, Mml::Math
xml do
element "document"
map_attribute "id", to: :id
map_element "math", to: :math
end
end
# 4. Parse works regardless of MyDocument's register
xml = <<~XML
<document id="test">
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mmultiscripts><mi>x</mi></mmultiscripts>
</math>
</document>
XML
doc = MyDocument.from_xml(xml)
doc.math.lutaml_register # => :mml_v2 (from lutaml_default_register)
doc.math.mmultiscripts_value.first.mi_value # => "x"Cross-register child resolution
When a parent model in one register embeds a child model from a different register, the child’s types must resolve in the child’s register context — not the parent’s. This is handled automatically by Register.resolve_for_child.
The problem
A :default-register parent that embeds a :mml_v2 child encounters UnknownTypeError when the child’s symbol types (e.g., :mmultiscripts) are looked up in :default instead of :mml_v2:
class MyDocument < Lutaml::Model::Serializable
attribute :math, Mml::V2::Math # Has lutaml_default_register = :mml_v2
xml do
element "document"
map_element "math", to: :math
end
end
# Without Register.resolve_for_child:
# UnknownTypeError: Unknown type 'mmultiscripts' in context 'default' The solution: Register.resolve_for_child
Register.resolve_for_child(child_class, parent_register) is the canonical API for determining which register to use for a child type. It is called automatically at every model entry point:
-
transformation_for— transformation compilation -
mappings_for— mapping resolution -
DeclarationPlanner— XML serialization namespace planning -
NamespaceCollector— XML serialization namespace collection -
TypeNamespaceResolver— type namespace reference resolution
You should rarely need to call it directly. The resolution policy is:
| Rule | Resolution |
|---|---|
Child has no | Use |
| Use child’s default |
| Use child’s default (no conflict) |
| Use child’s default (child knows better) |
`parent_register’s fallback chain includes child’s default | Use |
Otherwise | Use child’s default (child is self-contained) |
MML use case: versioned MathML schemas
The mml gem defines MathML v2 and v3 as separate registers, each with their own type set. A document embedding MathML uses lutaml_default_register on the version-specific base class:
module Mml
class V2Base < Lutaml::Model::Serializable
def self.lutaml_default_register
:mml_v2
end
end
class Math < V2Base
attribute :mmultiscripts, Mmultiscripts, collection: true
# :mmultiscripts type symbol resolves in :mml_v2 context
end
end
# Any document can embed Mml::V2::Math and it "just works"
class Article < Lutaml::Model::Serializable
attribute :formula, Mml::V2::Math
xml do
element "article"
map_element "math", to: :formula
end
end
doc = Article.from_xml(xml) # No explicit register needed
doc.formula.mmultiscripts.first.mi_value # => "x"XMI use case: namespace-bound version registers
The xmi gem defines registers for each XMI version (20131001, 20161101), bound to XML namespace URIs. A mixed-version XMI document may use types from multiple registers simultaneously.
When the XMI parser detects namespace versions at parse time, it may need to dynamically extend the fallback chain. Use Register#add_fallback for this — it coordinates both the Register-level fallback array AND the frozen TypeContext, and invalidates caches:
# Dynamically add a fallback at runtime (e.g., for mixed-version documents)
register.add_fallback(:xmi_20161101)
# This is equivalent to but safer than:
# register.fallback << :xmi_20161101 # UNSAFE: doesn't update TypeContext Never mutate register.fallback directly. Always use add_fallback to keep the Register and TypeContext in sync. |
Namespace-bound registers
A Register manages models and type resolution in a format-agnostic way — it has no knowledge of the serialization format (XML, YAML, JSON, etc.). However, the NamespaceBinding mechanism specifically works with Lutaml::Xml::Namespace subclasses. The format-agnostic design allows this to be extended in the future; for now, the binding API accepts XML namespace classes.
Namespace binding enables version-aware type resolution: the same type name can resolve to different classes depending on the namespace in the document. This is essential when different versions of a specification share the same type name but with different structures.
Core concept
A register can be bound to one or more Lutaml::Xml::Namespace subclasses. When resolving a type, the register checks whether the current XML namespace URI is one it handles and resolves accordingly. The binding is mediated by Lutaml::Model::NamespaceBinding, which stores the namespace class and its URI.
# Define namespace classes inheriting from Lutaml::Xml::Namespace
class Xmi20131001 < Lutaml::Xml::Namespace
uri "http://www.omg.org/spec/XMI/20131001"
prefix :xmi
element_form_default :qualified
end
# Create and bind a register to the namespace
register = Lutaml::Model::Register.new(:spec_20131001)
register.bind_namespace(Xmi20131001)
register.handles_namespace?("http://www.omg.org/spec/XMI/20131001")
# => true
Once bound, the register becomes the authoritative register for that namespace.
`GlobalContext` maintains a bidirectional mapping so that namespace URIs can be
looked up to find the correct register, and vice versa.
=== Bind a register to namespaces
Use `Register#bind_namespace` to bind multiple `Lutaml::Xml::Namespace` subclasses to the
same register. Each namespace class contributes its URI to the register's binding map:
[source,ruby]register.bind_namespace(Xmi20131001) register.bind_namespace(Uml20131001) register.bind_namespace(UmlDi20131001) register.bind_namespace(UmlDc20131001)
Each namespace URI maps to a binding in the register
register.bound_namespace_uris # ⇒ [ # "http://www.omg.org/spec/XMI/20131001", # "http://www.omg.org/spec/UML/20131001", # "http://www.omg.org/spec/UML/20131001/UMLDI", # "http://www.omg.org/spec/UML/20131001/UMLDC" # ]
=== Namespace-aware type resolution Use `resolve_in_namespace` to resolve a type given an XML namespace URI. The XML parsing infrastructure supplies the namespace URI at parse time when building model objects, ensuring the correct version-specific class is instantiated: [source,ruby]
Resolve :documentation in the context of the XMI 20131001 namespace
klass = register.resolve_in_namespace(:documentation, "http://www.omg.org/spec/XMI/20131001") # Returns the class registered under :documentation in this register
Without a namespace, falls back to normal resolution through the fallback chain
register.resolve_in_namespace(:documentation, nil)
=== Hierarchical fallback with namespace bindings When a newer specification version extends an older one (e.g., XMI 20161101 extends XMI 20131001), the newer register falls back to the older register: [source,ruby]
Older version
v20131001 = Lutaml::Model::Register.new(:xmi_20131001) v20131001.bind_namespace(Xmi20131001) v20131001.register_model(V20131001::Documentation, id: :documentation)
Newer version extends older version
v20161101 = Lutaml::Model::Register.new(:xmi_20161101, fallback: [:xmi_20131001]) v20161101.bind_namespace(Xmi20161101) v20161101.register_model(V20161101::Extension, id: :extension)
V20161101::Extension is registered locally; V20131001::Documentation is
found via fallback chain resolution
WARNING: Do not create circular fallback chains. If register A falls back to B, and B already falls back to A (directly or indirectly), type resolution will enter an infinite loop. Always verify the fallback chain before use. === Mixed namespace documents Some XML documents use different namespace versions for XMI, UML, UMLDI, and UMLDC. For example, an XMI document may declare: [source,xml]
<xmi:XMI xmlns:xmi="http://www.omg.org/spec/XMI/20131001" xmlns:uml="http://www.omg.org/spec/UML/20161101">
Here the XMI namespace is version 20131001, but the UML namespace is 20161101. When parsing this document, the XMI register must: 1. Handle the XMI 20131001 namespace (primary) 2. Also handle the UML 20161101 namespace (additional) 3. Fall back to the UML 20161101 register for types specific to that version This is handled by *extending the fallback chain* for additional namespace versions. Given a primary register and a list of all detected versions: [source,ruby]
Detect all namespace versions from the document
versions = NamespaceDetector.detect_versions(xml_content) # ⇒ { xmi: "20131001", uml: "20161101", umldi: nil, umldc: nil }
primary_register = register_for_version(versions[:xmi]) # xmi_20131001 all_versions = [versions[:xmi], versions[:uml]].compact.uniq
For each additional version, bind its namespace URIs and extend the fallback
all_versions.drop(1).each do |ver| reg = register_for_version(ver) # xmi_20161101 reg.bound_namespace_uris.each do |uri| primary_register.bind_namespace(resolve_namespace_class(uri)) end primary_register.fallback << reg.id unless reg.fallback.include?(primary_register.id) end
This ensures: * The primary register handles all namespace URIs present in the document * Type resolution searches the fallback chain for version-specific types * No cycles are introduced (guarded by checking whether the additional register's fallback already includes the primary register) === GlobalContext namespace registry `GlobalContext` maintains the global namespace-to-register mapping: [source,ruby]
Bind a register to a namespace in GlobalContext
register.bind_namespace(XmiNamespace) # This also calls GlobalContext.bind_register_to_namespace internally
Look up which register handles a namespace
Lutaml::Model::GlobalContext.register_for_namespace( "http://www.omg.org/spec/XMI/20131001" ) # ⇒ the register bound to XmiNamespace
The `GlobalContext#register_for_namespace` method is the central lookup used by the parsing infrastructure to route XML elements to the correct register based on their namespace URI. === Complete example: versioned schema with XML namespace bindings Here is a complete example showing how to set up a registry hierarchy with namespace bindings, suitable for a multi-version XML specification: [source,ruby]
1. Define Xml::Namespace subclasses for each version
class Xmi20131001 < Lutaml::Xml::Namespace uri "http://www.omg.org/spec/XMI/20131001" prefix :xmi end
class Xmi20161101 < Lutaml::Xml::Namespace uri "http://www.omg.org/spec/XMI/20161101" prefix :xmi end
2. Create version-specific registers with fallback chains
v20131001 = Lutaml::Model::Register.new(:spec_20131001) v20131001.bind_namespace(Xmi20131001)
v20161101 = Lutaml::Model::Register.new(:spec_20161101, fallback: [:spec_20131001]) v20161101.bind_namespace(Xmi20161101)
3. Register models in each version
v20131001.register_model(V20131001::Documentation, id: :documentation) v20131001.register_model(V20131001::Extension, id: :extension)
v20161101.register_model(V20161101::Extension, id: :extension) # different structure
4. Register in GlobalRegister for global lookup
Lutaml::Model::GlobalRegister.instance.register(v20131001) Lutaml::Model::GlobalRegister.instance.register(v20161101)
5. Parse with the appropriate register
doc = MyModel.from_xml(xml_content, register: v20161101)
6. Detect the right register from document content
detected = MyVersionRegistry.detect_register(xml_content) # Returns the appropriate register, with namespaces and fallbacks # already configured for the document doc = MyModel.from_xml(xml_content, register: detected)