Lutaml::Model is the Ruby implementation of the LutaML modeling methodology, for:
-
creating information models in the LutaML language (or its Ruby DSL)
-
serializing and deserializing LutaML information models
-
accessing data instances of LutaML information models
-
documenting LutaML information models
It provides simple, flexible and comprehensive mechanisms for defining information models with attributes and types, and the serialization of them to/from serialization formats including Hash, JSON, XML, YAML, and TOML.
For serialization formats, it uses an adapter pattern to support multiple libraries for each format, providing flexibility and extensibility for your data modeling needs.
Note
|
The Lutaml::Model modeling Ruby DSL was originally designed to be mostly compatible with the data modeling DSL of Shale, a data modeller for Ruby. Lutaml::Model is meant to address advanced needs not currently addressed by Shale. Instructions on how to migrate from Shale to Lutaml::Model are provided in Migration steps from Shale. |
-
Define models with attributes and types
-
Serialize and deserialize models to/from Hash, JSON, XML, YAML, and TOML
-
Support for multiple serialization libraries (e.g.,
toml-rb
,tomlib
) -
Configurable adapters for different serialization formats
-
Support for collections and default values
-
Custom serialization/deserialization methods
-
XML namespaces and mappings
-
Create custom adapters for additional data formats (see Custom Adapters)
Data modeling is the process of creating a data model for the data to be stored in a database or used in an application. It helps in defining the structure, relationships, and constraints of the data, making it easier to manage and use.
Lutaml::Model simplifies data modeling in Ruby by allowing you to define models with attributes and serialize/deserialize them to/from various serialization formats seamlessly.
The Lutaml::Model data modelling approach is as follows:
LutaML Model
│
Has many attributes
│
▼
Attribute
│
Has type of
│
┌──────────┴──────────┐
│ │
Model Value (Leaf)
│ │
Has many attributes Contains one basic value
│ │
┌───────┴─────┐ ┌──────┴──────┐
│ │ │ │
Model Value (Leaf) String Integer
│ Date Boolean
│ Time Float
Has many attributes ... ...
│
▼
(Recursive pattern continues...)
Studio (Model)
├── name (Value: String) = "Pottery Studio"
├── address (Model)
│ ├── street (Value: String) = "123 Clay St"
│ ├── city (Value: String) = "Ceramics City"
│ └── postcode (Value: String) = "12345"
├── established (Value: Date) = 2020-01-01
└── kilns (Model)
├── count (Value: Integer) = 3
└── temperature (Value: Float) = 1200.0
╔═══════════════════════╗ ╔════════════════════════════╗
║ LutaML Core Model ║ ║ Serialization Models ║
╚═══════════════════════╝ ╚════════════════════════════╝
╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆ Model ┆ ┆ XML Model ┆
┆ │ ┆ ┌────────────────┐ ┆ │ ┆
┆ ┌────────┴──┐ ┆ │ │ ┆ ┌──────┴──────┐ ┆
┆ │ │ ┆ │ Model │ ┆ │ │ ┆
┆ Models Value Types ┆──►│ Transformation │ ┆ Models Value Types ┆
┆ │ │ ┆ │ & │ ┆ │ │ ┆
┆ │ │ ┆ │ Mapping Rules │ ┆ │ │ ┆
┆ │ ┌──────┴──┐ ┆ │ │ ┆ ┌────┴────┐ ┌─┴─┐ ┆
┆ │ │ │ ┆ └────────────────┘ ┆ │ │ │ │ ┆
┆ │ String Integer ┆ │ ┆ Element Value xs:string ┆
┆ │ Date Float ┆ │ ┆ Attribute Type xs:date ┆
┆ │ Time Boolean ┆ ├──────────►┆ xs:boolean ┆
┆ │ ┆ │ ┆ xs:anyURI ┆
┆ └──────┐ ┆ │ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
┆ │ ┆ │
┆ Contains ┆ │ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆ more Models ┆ │ ┆ JSON Model ┆
┆ (recursive) ┆ │ ┆ │ ┆
┆ ┆ │ ┆ ┌──────┴──────┐ ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯ └──────────►┆ │ │ ┆
┆ Models Value Types ┆
┆ │ │ ┆
┆ │ │ ┆
┆ ┌────┴───┐ ┌───┴──┐ ┆
┆ │ │ │ │ ┆
┆ object array number string ┆
┆ value boolean null ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
╔═══════════════════════╗ ╔══════════════════╗ ╔═══════════════════════╗
║LutaML Model Class FOO ║ ║LutaML Transformer║ ║LutaML Model Class BAR ║
╚═══════════════════════╝ ╚══════════════════╝ ╚═══════════════════════╝
╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆ Model ┆ ┆ Model ┆
┆ │ ┆ ┌────────────────┐ ┆ │ ┆
┆ ┌────────┴──┐ ┆ │ │ ┆ ┌────────┴──┐ ┆
┆ │ │ ┆ │ Model │ ┆ │ │ ┆
┆ Models Value Types ┆───►│ Transformation │───►┆ Models Value Types ┆
┆ │ │ ┆◄───│ & │◄───┆ │ │ ┆
┆ │ │ ┆ │ Mapping Rules │ ┆ │ │ ┆
┆ │ ┌──────┴──┐ ┆ │ │ ┆ │ ┌──────┴──┐ ┆
┆ │ │ │ ┆ └────────────────┘ ┆ │ │ │ ┆
┆ │ String Integer ┆ ┆ │ String Integer ┆
┆ │ Date Float ┆ ┆ │ Date Float ┆
┆ │ Time Boolean ┆ ┆ │ Time Boolean ┆
┆ │ ┆ ┆ │ ┆
┆ └──────┐ ┆ ┆ └──────┐ ┆
┆ │ ┆ ┆ │ ┆
┆ Contains ┆ ┆ Contains ┆
┆ more Models ┆ ┆ more Models ┆
┆ (recursive) ┆ ┆ (recursive) ┆
┆ ┆ ┆ ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
Value
class, transformation, and serialization formats╔═══════════════════════╗ ╔═══════════════════════╗
║LutaML Value Class FOO ║ ║ Serialization Value ║
╚═══════════════════════╝ ╚═══════════════════════╝
╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆ ┌───────────────┐ ┆ ┆ ┌───────────────┐ ┆
┆ │ Value │ ┆ ┌──────────────────┐ ┆ │ XML Value │ ┆
┆ └───────────────┘ ┆──►│ Value Serializer │──►┆ └───────────────┘ ┆
┆ ┌───────────────┐ ┆ └──────────────────┘ ┆ ┌───────────────┐ ┆
┆ │Primitive Types│ ┆ ┆ │XML Value Types│ ┆
┆ └───────────────┘ ┆ ┆ └───────────────┘ ┆
┆ ┌───┘ ┆ ┆ ┌───┘ ┆
┆ ├─ string ┆ ┆ ├─ xs:string ┆
┆ ├─ integer ┆ ┆ ├─ xs:integer ┆
┆ ├─ float ┆ ┆ ├─ xs:decimal ┆
┆ ├─ boolean ┆ ┆ ├─ xs:boolean ┆
┆ ├─ date ┆ ┆ ├─ xs:date ┆
┆ ├─ time_without_date ┆ ┆ ├─ xs:time ┆
┆ ├─ date_time ┆ ┆ ├─ xs:dateTime ┆
┆ ├─ time ┆ ┆ ├─ xs:decimal ┆
┆ ├─ decimal ┆ ┆ ├─ xs:anyType ┆
┆ └─ hash ┆ ┆ └─ (complex element) ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
│
▼
┌───────────────────┐
│ Value Transformer │
└───────────────────┘
│
▼
╔═══════════════════════╗
║LutaML Value Class BAR ║
╚═══════════════════════╝
╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆ ┌───────────────┐ ┆
┆ │ Value │ ┆
┆ └───────────────┘ ┆
┆ ┌───────────────┐ ┆
┆ │Primitive Types│ ┆
┆ └───────────────┘ ┆
┆ ┌───┘ ┆
┆ ├─ string ┆
┆ ├─ integer ┆
┆ ├─ float ┆
┆ ├─ boolean ┆
┆ ├─ date ┆
┆ ├─ time_without_date ┆
┆ ├─ date_time ┆
┆ ├─ time ┆
┆ ├─ decimal ┆
┆ └─ hash ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
╔═════════════════════╗ ╔═════════════════════╗ ╔═════════════════════╗
║ Studio (Core Model) ║ ║ JSON Model ║ ║ Serialized JSON ║
╚═════════════════════╝ ╚═════════════════════╝ ╚═════════════════════╝
name: "Studio 1" ┌─► { ┌─► {
address: │ "name": "...", │ "name": "Studio 1",
├── street: "..." │ "address": { │ "address": {
└── city: "..." │ "street": "...", │ "street": "...",
kilns: ──┤ "city": "..." ──┤ "city": "..."
├── count: 3 │ }, │ },
└── temp: 1200 │ "kilnsCount": ..., │ "kilnsCount": 3,
│ "kilnsTemp": ... │ "kilnsTemp": 1200
└─► } └─► }
Add this line to your application’s Gemfile:
gem 'lutaml-model'
And then execute:
bundle install
Or install it yourself as:
gem install lutaml-model
There are two ways to define an information model in Lutaml::Model:
-
Inheriting from the
Lutaml::Model::Serializable
class -
Including the
Lutaml::Model::Serialize
module
The simplest way to define a model is to create a class that inherits from
Lutaml::Model::Serializable
.
The attribute
class method is used to define attributes.
require 'lutaml/model'
class Kiln < Lutaml::Model::Serializable
attribute :brand, :string
attribute :capacity, :integer
attribute :temperature, :integer
end
If the model class already has a super class that it inherits from, the model
can be extended using the Lutaml::Model::Serialize
module.
require 'lutaml/model'
class Kiln < SomeSuperClass
include Lutaml::Model::Serialize
attribute :brand, :string
attribute :capacity, :integer
attribute :temperature, :integer
end
A model can inherit from another model to inherit all attributes and methods of the parent model, allowing for code reusability and a clear model hierarchy.
Syntax:
class Superclass < Lutaml::Model::Serializable
# attribute ...
# serialization blocks
end
class Subclass < Superclass
# attributes are additive
# serialization blocks are replaced
end
An inherited model has the following characteristics:
-
All attributes are inherited from the parent model.
-
Additional calls to
attribute
in the child model are additive, unless the attribute name is the same as an attribute in the parent model. -
Serialization blocks, such as
xml
andkey_value
are replaced when defined.-
In order to selectively import serialization mapping rules from the parent model, the
import_model_mappings
method can be used (see [import_model_mappings]).
-
A Serialize
/ Serializable
object can be compared with another object of the
same class using the ==
operator. This is implemented through the
ComparableModel
module.
Two objects are considered equal if they have the same class and all their attributes are equal. This behavior differs from the typical Ruby behavior, where two objects are considered equal only if they have the same object ID.
Note
|
Two Serialize objects will have the same hash value if they have the
same class and all their attributes are equal.
|
> a = Kiln.new(brand: 'Kiln 1', capacity: 100, temperature: 1050)
> b = Kiln.new(brand: 'Kiln 1', capacity: 100, temperature: 1050)
> a == b
> # true
> a.hash == b.hash
> # true
Lutaml::Model supports the following attribute value types.
Every type has a corresponding Ruby class and a serialization format type.
Lutaml::Model::Type | Ruby class | XML | JSON | YAML | Example value |
---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
complex element |
object |
map |
|
(nil value) |
|
|
|
|
|
Warning
|
Decimal is an optional feature. |
The Decimal type is a value type that is disabled by default.
Note
|
The reason why the Decimal type is disabled by default is that the
BigDecimal class became optional to the standard Ruby library from Ruby 3.4
onwards. The Decimal type is only enabled when the bigdecimal library is
loaded.
|
The following code needs to be run before using (and parsing) the Decimal type:
require 'bigdecimal'
If the bigdecimal
library is not loaded, usage of the Decimal
type will
raise a Lutaml::Model::TypeNotSupportedError
.
A custom class can be used as an attribute type. The custom class must inherit
from Lutaml::Model::Type::Value
or a class that inherits from it.
A class inheriting from the Value
class carries the attribute value
which
stores the one-and-only "true" value that is independent of serialization
formats.
The minimum requirement for a custom class is to implement the following methods:
self.cast(value)
-
Assignment of an external value to the
Value
class to be set asvalue
. Casts the value to the custom type. self.serialize(value)
-
Serializes the custom type to an object (e.g. a string). Takes the internal
value
and converts it into an output suitable for serialization.
class FiveDigitPostCode < Lutaml::Model::Type::String
def self.cast(value)
value = value.to_s if value.is_a?(Integer)
unless value.is_a?(::String)
raise Lutaml::Model::InvalidValueError, "Invalid value for type 'FiveDigitPostCode'"
end
# Pad zeros to the left
value.rjust(5, '0')
end
def self.serialize(value)
value
end
end
class Studio < Lutaml::Model::Serializable
attribute :postcode, FiveDigitPostCode
end
The serialization of custom types can be made to differ per serialization format
by defining methods in the class definitions. This requires additional methods
than the minimum required for a custom class (i.e. self.cast(value)
and
self.serialize(value)
).
This is useful in the case when different serialization formats of the same model expect differentiated value representations.
The methods that can be overridden are named:
self.from_{format}(serialized_string)
-
Deserializes a string of the serialization format and returns the object to be assigned to the
Value
class'value
. to_{format}
-
Serializes the object to a string of the serialization format.
The {format}
part of the method name is the serialization format in lowercase
(e.g. hash
, json
, xml
, yaml
, toml
).
Suppose in XML we handle a high-precision date-time type that requires custom serialization methods, but other formats such as JSON do not support this type.
For instance, in the normal DateTime class, the serialized string is
2012-04-07T01:51:37+02:00
, and the high-precision format is
2012-04-07T01:51:37.112+02:00
.
We create HighPrecisionDateTime
class is a custom class that inherits
from Lutaml::Model::Type::DateTime
.
class HighPrecisionDateTime < Lutaml::Model::Type::DateTime
# Inherit the `self.cast(value)` and `self.serialize(value)` methods
# from Lutaml::Model::Type::DateTime
# The format looks like this `2012-04-07T01:51:37.112+02:00`
def self.from_xml(xml_string)
::DateTime.parse(xml_string)
end
# The %L adds milliseconds to the time
def to_xml
value.strftime('%Y-%m-%dT%H:%M:%S.%L%:z')
end
end
class Ceramic < Lutaml::Model::Serializable
attribute :kiln_firing_time, HighPrecisionDateTime
xml do
root 'ceramic'
map_element 'kilnFiringTime', to: :kiln_firing_time
# ...
end
end
An XML snippet with the high-precision date-time type:
<ceramic>
<kilnFiringTime>2012-04-07T01:51:37.112+02:00</kilnFiringTime>
<!-- ... -->
</ceramic>
When loading the XML snippet, the HighPrecisionDateTime
class will be used to
parse the high-precision date-time string.
However, when serializing to JSON, the value will have the high-precision part lost due to the inability of JSON to handle high-precision date-time.
> c = Ceramic.from_xml(xml)
> #<Ceramic:0x0000000104ac7240 @kiln_firing_time=#<HighPrecisionDateTime:0x0000000104ac7240 @value=2012-04-07 01:51:37.112000000 +0200>>
> c.to_json
> # {"kilnFiringTime":"2012-04-07T01:51:37+02:00"}
An attribute is the basic building block of a model. It is a named value that stores a single piece of data (which may be one or multiple pieces of data).
An attribute only accepts the type of value defined in the attribute definition.
The attribute value type can be one of the following:
-
Value (inherits from Lutaml::Model::Value)
-
Model (inherits from Lutaml::Model::Serializable)
Syntax:
attribute :name_of_attribute, Type
Where,
name_of_attribute
-
The defined name of the attribute.
Type
-
The type of the attribute.
attribute
class method to define simple attributesclass Studio < Lutaml::Model::Serializable
attribute :name, :string
attribute :address, :string
attribute :established, :date
end
s = Studio.new(name: 'Pottery Studio', address: '123 Clay St', established: Date.new(2020, 1, 1))
puts s.name
#=> "Pottery Studio"
puts s.address
#=> "123 Clay St"
puts s.established
#=> <Date: 2020-01-01>
A polymorphic attribute is an attribute that can accept multiple types of values. This is useful when the attribute defines common characteristics and behaviors among different types.
An attribute with a defined value type also accepts values that are of a class that is a subclass of the defined type.
The assigned attribute of Type
accepts polymorphic classes as long as the
assigned instance is of a class that either inherits from the declared type or
matches it.
A naïve polymorphic approach is to define an attribute with a superclass type and assign instances of subclasses to it.
While this approach works (somewhat) in modeling, it does not work with serialization (half) or deserialization (not at all).
The following example illustrates why such approach is naïve.
class Studio < Lutaml::Model::Serializable
attribute :name, :string
end
# CeramicStudio is a specialization of Studio
class CeramicStudio < Studio
attribute :clay_type, :string
end
class PotteryClass < Lutaml::Model::Serializable
# the :studio attribute should accept Studio and CeramicStudio
attribute :studio, Studio
end
# This works
> s = Studio.new(name: 'Pottery Studio')
> p = PotteryClass.new(studio: s)
> p.studio
# => <Studio:0x0000000104ac7240 @name="Pottery Studio", @address=nil, @established=nil>
# A subclass of Studio is also valid
> s = CeramicStudio.new(name: 'Ceramic World', clay_type: 'Red')
> p = PotteryClass.new(studio: s)
> p.studio
# => <CeramicStudio:0x0000000104ac7240 @name="Ceramic World", @address=nil, @established=nil, @clay_type="Red">
> p.studio.name
# => "Ceramic World"
> p.studio.clay_type
# => "Red"
So far so good. However, this approach does not work in serialization.
This is what happens when we call to_yaml
on the PotteryClass
instance.
> puts p.to_yaml
# => ---
# => studio:
# => name: Ceramic World
# => clay_type: Red
When deserializing the YAML string, the studio
attribute will be deserialized
as an instance of Studio
, not CeramicStudio
. This means that the clay_type
attribute will be lost.
> p = PotteryClass.load_yaml("---\nstudio:\n name: Ceramic World\n clay_type: Red")
> p.studio
# => <Studio:0x0000000104ac7240 @name="Ceramic World">
> p.studio.clay_type
# => ERROR
Lutaml::Model offers rich support for polymorphic attributes, through configuration at both attribute and serialization levels.
In polymorphism, there are the following components:
- polymorphic attribute
-
the attribute that can be assigned multiple types.
- polymorphic attribute class
-
the class that has a polymorphic attribute.
- polymorphic superclass
-
a class assigned to a polymorphic attribute that serves as the superclass for all accepted polymorphic classes.
- polymorphic subclass
-
a class that is a subclass of the polymorphic superclass and can be assigned to the polymorphic attribute. There are often more than 2 subclasses in a scenario since polymorphism is meant to apply to multiple types.
To utilize polymorphic attributes, modification to all of these components are necessary.
In serialized form, polymorphic classes are differentiated by an explicit "polymorphic class differentiator".
In key-value formats like YAML, the polymorphic class differentiator is typically a key-value pair that contains the polymorphic class name.
references:
- _class: Document # This is a DocumentReference
name: "The Tibetan Book of the Dead"
document_id: "book:tbtd"
- _class: Anchor # This is an AnchorReference
name: "Chapter 1"
anchor_id: "book:tbtd:anchor-1"
In XML, the polymorphic class differentiator is typically an attribute that contains the polymorphic class name.
<references>
<!-- The "document-ref" value is a DocumentReference -->
<reference reference-type="document-ref">
<name>The Tibetan Book of the Dead</name>
<document_id>book:tbtd</document_id>
</reference>
<!-- The "anchor-ref" value is an AnchorReference -->
<reference reference-type="anchor-ref">
<name>Chapter 1</name>
<anchor_id>book:tbtd:anchor-1</anchor_id>
</reference>
</references>
Note
|
While it is possible to determine different polymorphic classes based on the attributes they contain, such mechanism would not be able to determine the polymorphic class if serializations of two polymorphic subclasses can be identical. |
There are two basic scenarios in using polymorphic attributes:
-
Scenario 1: Setting polymorphism in the polymorphic superclass:
-
Scenario 2: Setting polymorphism in the individual polymorphic subclasses:
Note
|
Please refer to spec/lutaml/model/polymorphic_spec.rb for full examples
of implementing polymorphic attributes.
|
The polymorphic attribute class is a class that has a polymorphic attribute.
At this level, the polymorphic
option is used to specify the types that the
polymorphic attribute can accept.
class PolymorphicAttributeClass < Lutaml::Model::Serializable
attribute :attribute_name, (1)
{polymorphic-superclass-class}, (2)
{options}, (3)
polymorphic: [ (4)
polymorphic-subclass-1, (5)
polymorphic-subclass-2,
]
end
-
The name of the polymorphic attribute.
-
The polymorphic superclass class.
-
Any options for the attribute.
-
The
polymorphic
option that determines the acceptable polymorphic subclasses. -
The polymorphic subclasses.
The polymorphic
option is an array of polymorphic subclasses that the
attribute can accept.
These options enable the following scenarios.
-
If the polymorphic attribute is to only contain instances of the
polymorphic-superclass-class
, not its subclasses, then thepolymorphic
option is not needed.In the following code,
ReferenceSet
has an attributereferences
that only accepts instances ofReference
. Thepolymorphic
option does not apply.class ReferenceSet < Lutaml::Model::Serializable attribute :references, Reference, collection: true end
-
If the attribute (collection or not) is meant to only contain one type of polymorphic subclasses, then the
polymorphic
option is also not needed, because the polymorphic subclass can be stated as the attribute value type.In the following code,
ReferenceSet
has an attributereferences
that only accepts instances ofDocumentReference
, a subclass ofReference
. Thepolymorphic
option does not apply.class ReferenceSet < Lutaml::Model::Serializable attribute :references, DocumentReference, collection: true end
-
If the attribute (collection or not) is meant to contain instances belonging to more than one polymorphic subclass, then those acceptable polymorphic subclasses should be explicitly specified in the
polymorphic: […]
option.In the following code,
ReferenceSet
is a class that has a polymorphic attributereferences
. Thereferences
attribute can accept instances ofDocumentReference
andAnchorReference
, both of which are subclasses ofReference
.class ReferenceSet < Lutaml::Model::Serializable attribute :references, Reference, collection: true, polymorphic: [ DocumentReference, AnchorReference, ] end
A polymorphic subclass needs an additional attribute with the
polymorphic_class
option to allow Lutaml::Model for identifying itself in
serialization. This attribute is called the "polymorphic class differentiator".
There are two methods for setting the polymorphic class differentiator:
-
Setting the polymorphic class differentiator in the polymorphic superclass, as polymorphic subclasses inherit from it (relying on Inheritance).
-
Setting the polymorphic class differentiator in the individual polymorphic subclasses
The polymorphic class differentiator can be set in the polymorphic superclass. This scenario fits best if there are many polymorphic subclasses and the polymorphic superclass can be modified.
Syntax:
class PolymorphicSuperclass < Lutaml::Model::Serializable
attribute :{_polymorphic_differentiator}, (1)
:string, (2)
polymorphic_class: true (3)
# ...
end
-
The polymorphic differentiator is a normal attribute that can be assigned to any name.
-
The polymorphic differentiator must have a value type of
:string
. -
The option for
polymorphic_class
must be set totrue
to indicate that this attribute accepts subclass types.
The polymorphic class differentiator can be set in the individual polymorphic subclasses. This scenario fits best if there are few polymorphic subclasses and the polymorphic superclass cannot be modified.
Syntax:
# No modification to the superclass is needed.
class PolymorphicSuperclass < Lutaml::Model::Serializable
# ...
end
# The polymorphic differentiator is set in the subclass.
class PolymorphicSubclass < PolymorphicSuperclass
attribute
:{_polymorphic_differentiator}, (1)
:string, (2)
polymorphic_class: true (3)
# ...
end
-
The polymorphic differentiator is a normal attribute that can be assigned to any name.
-
The polymorphic differentiator must have a value type of
:string
. -
The option for
polymorphic_class
must be set totrue
to indicate that this attribute accepts subclass types.
The polymorphic attribute class needs to determine what class to use based on the serialized value of the polymorphic differentiator.
The polymorphic attribute class mapping is format-independent, allowing for differentiation of polymorphic subclasses in different serialization formats.
The mapping of the serialized polymorphic differentiator can be set in either:
-
the polymorphic superclass; or
-
the polymorphic attribute class and the individual polymorphic subclasses.
This use case applies when the polymorphic superclass can be modified, and that polymorphism is intended to apply to all its subclasses.
This is done through the polymorphic_map
option in the serialization blocks
inside the polymorphic attribute class.
Syntax:
class PolymorphicSuperclass < Lutaml::Model::Serializable
attribute :{_polymorphic_differentiator}, :string, polymorphic_class: true
xml do
(map_attribute | map_element) "XmlPolymorphicAttributeName", (1)
to: :{_polymorphic_differentiator}, (2)
polymorphic_map: { (3)
"xml-value-for-subclass-1" => PolymorphicSubclass1, (4)
"xml-value-for-subclass-2" => PolymorphicSubclass2,
}
end
(key_value | key_value_format) do
map "KeyValuePolymorphicAttributeName", (5)
to: :{_polymorphic_differentiator}, (6)
polymorphic_map: {
"keyvalue-value-for-subclass-1" => PolymorphicSubclass1,
"keyvalue-value-for-subclass-2" => PolymorphicSubclass2,
}
end
end
class PolymorphicSubclass1 < PolymorphicSuperclass
# ...
end
class PolymorphicSubclass2 < PolymorphicSuperclass
# ...
end
class PolymorphicAttributeClass < Lutaml::Model::Serializable
attribute :polymorphic_attribute,
PolymorphicSuperclass,
{options},
polymorphic: [
PolymorphicSubclass1,
PolymorphicSubclass2,
]
# ...
end
-
The name of the XML element or attribute that contains the polymorphic differentiator.
-
The name of the polymorphic differentiator attribute defined in
attribute
with thepolymorphic
option. -
The
polymorphic_map
option that determines the class to use based on the value of the differentiator. -
The mapping of the differentiator value to the polymorphic subclass.
-
The name of the key-value element that contains the polymorphic differentiator.
-
The name of the polymorphic differentiator attribute defined in
attribute
with thepolymorphic
option.
class Reference < Lutaml::Model::Serializable
attribute :_class, :string, polymorphic_class: true
attribute :name, :string
xml do
map_attribute "reference-type", to: :_class, polymorphic_map: {
"document-ref" => "DocumentReference",
"anchor-ref" => "AnchorReference",
}
map_element "name", to: :name
end
key_value do
map "_class", to: :_class, polymorphic_map: {
"Document" => "DocumentReference",
"Anchor" => "AnchorReference",
}
map "name", to: :name
end
end
class DocumentReference < Reference
attribute :document_id, :string
xml do
map_element "document_id", to: :document_id
end
key_value do
map "document_id", to: :document_id
end
end
class AnchorReference < Reference
attribute :anchor_id, :string
xml do
map_element "anchor_id", to: :anchor_id
end
key_value do
map "anchor_id", to: :anchor_id
end
end
class ReferenceSet < Lutaml::Model::Serializable
attribute :references, Reference, collection: true, polymorphic: [
DocumentReference,
AnchorReference,
]
end
---
references:
- _class: Document
name: The Tibetan Book of the Dead
document_id: book:tbtd
- _class: Anchor
name: Chapter 1
anchor_id: book:tbtd:anchor-1
<ReferenceSet>
<references reference-type="document-ref">
<name>The Tibetan Book of the Dead</name>
<document_id>book:tbtd</document_id>
</references>
<references reference-type="anchor-ref">
<name>Chapter 1</name>
<anchor_id>book:tbtd:anchor-1</anchor_id>
</references>
</ReferenceSet>
This use case applies when the polymorphic superclass is not meant to be modified.
This is done through the polymorphic_map
option in the serialization blocks
inside the polymorphic attribute class, and the polymorphic
option in the
individual polymorphic subclasses.
In this scenario, similar to the previous case where the polymorphic differentiator is set at the polymorphic superclass, the following conditions must be satisifed:
-
the polymorphic differentiator attribute name must be the same across polymorphic subclasses
If the model polymorphic differentiator in one polymorphic subclass is
_ref_type
, then it must be so in all other polymorphic subclasses. -
the polymorphic differentiator in the serialization formats must be identical within the polymorphic subclasses of that serialization format.
If the XML polymorphic differentiator is
reference-type
, then it must be so in the XML of all polymorphic subclasses.
Syntax:
# Assume that we have no access to the base class and we need to define
# polymorphism in the sub-classes.
class PolymorphicSuperclass < Lutaml::Model::Serializable
end
class PolymorphicSubclass1 < PolymorphicSuperclass
attribute :_polymorphic_differentiator, :string
xml do
(map_attribute | map_element) "XmlPolymorphicAttributeName", (1)
to: :_polymorphic_differentiator
end
(key_value | key_value_format) do
map "KeyValuePolymorphicAttributeName", (2)
to: :_polymorphic_differentiator
end
end
class PolymorphicSubclass2 < PolymorphicSuperclass
attribute :_polymorphic_differentiator, :string
xml do
(map_attribute | map_element) "XmlPolymorphicAttributeName2",
to: :_polymorphic_differentiator
end
(key_value | key_value_format) do
map "KeyValuePolymorphicAttributeName2",
to: :_polymorphic_differentiator
end
end
class PolymorphicAttributeClass < Lutaml::Model::Serializable
attribute :polymorphic_attribute,
PolymorphicSuperclass,
{options},
polymorphic: [
PolymorphicSubclass1,
PolymorphicSubclass2,
] (3)
# ...
xml do
map_element "XmlPolymorphicElement", (4)
to: :polymorphic_attribute,
polymorphic: { (5)
# This refers to the polymorphic differentiator attribute in the polymorphic subclass.
attribute: :_polymorphic_differentiator, (6)
class_map: { (7)
"xml-i-am-subclass-1" => "PolymorphicSubclass1",
"xml-i-am-subclass-2" => "PolymorphicSubclass2",
},
}
end
(key_value | key_value_format) do
map "KeyValuePolymorphicAttributeName", (8)
to: :polymorphic_attribute,
polymorphic: { (9)
attribute: :_polymorphic_differentiator, (10)
class_map: { (11)
"keyvalue-i-am-subclass-1" => "PolymorphicSubclass1",
"keyvalue-i-am-subclass-2" => "PolymorphicSubclass2",
},
}
end
end
-
The name of the XML element or attribute that contains the polymorphic differentiator.
-
The name of the key-value element that contains the polymorphic differentiator.
-
Definition of the polymorphic attribute and the polymorphic subclasses in the polymorphic attribute class.
-
The name of the XML element that contains the polymorphic attributes. This must be an element as a polymorphic attribute must be a model.
-
The
polymorphic
option on a mapping defines necessary information for polymorphic serialization. -
The
attribute:
name of the polymorphic differentiator attribute defined in the polymorphic subclass. -
The
class_map:
option that determines the polymorphic subclass to use based on the value of the differentiator. -
The name of the key-value format key that contains the polymorphic attributes.
-
Same as <5>, but for the key-value format.
-
Same as <6>, but for the key-value format.
-
Same as <7>, but for the key-value format.
class Reference < Lutaml::Model::Serializable
attribute :name, :string
end
class DocumentReference < Reference
attribute :_class, :string
attribute :document_id, :string
xml do
map_element "document_id", to: :document_id
map_attribute "reference-type", to: :_class
end
key_value do
map "document_id", to: :document_id
map "_class", to: :_class
end
end
class AnchorReference < Reference
attribute :_class, :string
attribute :anchor_id, :string
xml do
map_element "anchor_id", to: :anchor_id
map_attribute "reference-type", to: :_class
end
key_value do
map "anchor_id", to: :anchor_id
map "_class", to: :_class
end
end
class ReferenceSet < Lutaml::Model::Serializable
attribute :references, Reference, collection: true, polymorphic: [
DocumentReference,
AnchorReference,
]
xml do
root "ReferenceSet"
map_element "reference", to: :references, polymorphic: {
# This refers to the attribute in the polymorphic model, you need
# to specify the attribute name (which is specified in the sub-classed model).
attribute: "_class",
class_map: {
"document-ref" => "DocumentReference",
"anchor-ref" => "AnchorReference",
},
}
end
key_value do
map "references", to: :references, polymorphic: {
attribute: "_class",
class_map: {
"Document" => "DocumentReference",
"Anchor" => "AnchorReference",
},
}
end
end
---
references:
- _class: Document
name: The Tibetan Book of the Dead
document_id: book:tbtd
- _class: Anchor
name: Chapter 1
anchor_id: book:tbtd:anchor-1
<ReferenceSet>
<reference reference-type="document-ref">
<name>The Tibetan Book of the Dead</name>
<document_id>book:tbtd</document_id>
</reference>
<reference reference-type="anchor-ref">
<name>Chapter 1</name>
<anchor_id>book:tbtd:anchor-1</anchor_id>
</reference>
</ReferenceSet>
Define attributes as collections (arrays or hashes) to store multiple values
using the collection
option.
When defining a collection attribute, it is important to understand the default initialization behavior and how to customize it.
By default, collections are initialized as nil
. However, if you want the collection to be initialized as an empty array, you can use the initialize_empty: true
option.
collection
can be set to:
true
-
The attribute contains an unbounded collection of objects of the declared class.
{min}..{max}
-
The attribute contains a collection of objects of the declared class with a count within the specified range. If the number of objects is out of this numbered range,
CollectionCountOutOfRangeError
will be raised.When set to
0..1
, it means that the attribute is optional, it could be empty or contain one object of the declared class.When set to
1..
(equivalent to1..Infinity
), it means that the attribute must contain at least one object of the declared class and can contain any number of objects.When set to 5..10` means that there is a minimum of 5 and a maximum of 10 objects of the declared class. If the count of values for the attribute is less then 5 or greater then 10, the
CollectionCountOutOfRangeError
will be raised.
Syntax:
attribute :name_of_attribute, Type, collection: true
attribute :name_of_attribute, Type, collection: {min}..{max}
attribute :name_of_attribute, Type, collection: {min}..
collection
option to define a collection attributeclass Studio < Lutaml::Model::Serializable
attribute :location, :string
attribute :potters, :string, collection: true
attribute :address, :string, collection: 1..2
attribute :hobbies, :string, collection: 0..
end
> Studio.new
> # address count is `0`, must be between 1 and 2 (Lutaml::Model::CollectionCountOutOfRangeError)
> Studio.new({ address: ["address 1", "address 2", "address 3"] })
> # address count is `3`, must be between 1 and 2 (Lutaml::Model::CollectionCountOutOfRangeError)
> Studio.new({ address: ["address 1"] }).potters
> # []
> Studio.new({ address: ["address 1"] }).address
> # ["address 1"]
> Studio.new(address: ["address 1"], potters: ['John Doe', 'Jane Doe']).potters
> # ['John Doe', 'Jane Doe']
# Default to `nil`
class SomeModel < Lutaml::Model::Serializable
attribute :coll, :string, collection: true
xml do
root "some-model"
map_element 'collection', to: :coll
end
key_value do
map 'collection', to: coll
end
end
puts SomeModel.new.coll
# => nil
puts SomeModel.new.to_xml
# =>
# <some-model xsi:xmlns="..."><collection xsi:nil="true"/></some-model>
puts SomeModel.new.to_yaml
# =>
# ---
# coll: null
# Default to empty array
class SomeModel < Lutaml::Model::Serializable
attribute :coll, :string, collection: true, initialize_empty: true
xml do
map_element 'collection', to: :coll
end
key_value do
map 'collection', to: coll
end
end
puts SomeModel.new.coll
# => []
puts SomeModel.new.to_xml
# =>
# <some-model><collection/></some-model>
puts SomeModel.new.to_yaml
# =>
# ---
# coll: []
A derived attribute is computed dynamically based on an instance method instead of storing a static value. It is defined using the method:
option.
Syntax:
attribute :name_of_attribute, method: :instance_method_name
class Invoice < Lutaml::Model::Serializable
attribute :subtotal, :float
attribute :tax, :float
attribute :total, method: :total_value
def total_value
subtotal + tax
end
end
i = Invoice.new(subtotal: 100.0, tax: 12.0)
i.total
#=> 112.0
puts i.to_yaml
#=> ---
#=> subtotal: 100.0
#=> tax: 12.0
#=> total: 112.0
The choice
directive allows specifying that elements from the specified range are included.
Note
|
Attribute-level definitions are supported. This can be used with both
key_value and xml mappings.
|
Syntax:
choice(min: {min}, max: {max}) do
{block}
end
Where,
min
-
The minimum number of elements that must be included. The minimum value can be
0
. max
-
The maximum number of elements that can be included. The maximum value can go up to
Float::INFINITY
. block
-
The block of elements that must be included. The block can contain multiple
attribute
andchoice
directives.
choice
directive to define a set of attributes with a rangeclass Studio < Lutaml::Model::Serializable
choice(min: 1, max: 3) do
choice(min: 1, max: 2) do
attribute :prefix, :string
attribute :forename, :string
end
attribute :completeName, :string
end
end
This means that the Studio
class must have at least one and at most three
attributes.
-
The first choice must have at least one and at most two attributes.
-
The second attribute is the
completeName
. -
The first choice can have either the
prefix
andforename
attributes or just theforename
attribute. -
The last attribute
completeName
is optional.
An importable model is a model that can be imported into another model using the
import_*
directive.
This feature works both with XML and key-value formats.
-
The import order determines how elements and attributes are overwritten.
-
An importable model with XML serialization mappings requires setting the model’s XML serialization configuration with the
no_root
directive.
The model can be imported into another model using the following directives:
import_model
-
imports both attributes and mappings.
import_model_attributes
-
imports only attributes.
import_model_mappings
-
imports only mappings.
Note
|
Models with no_root can only be parsed through parent models.
Direct calling NoRootModel.from_xml will raise a NoRootMappingError .
|
Note
|
Namespaces are not currently supported in importable models.
If namespace is defined with no_root , NoRootNamespaceError will be raised.
|
class GroupOfItems < Lutaml::Model::Serializable
attribute :name, :string
attribute :type, :string
attribute :code, :string
xml do
no_root
sequence do
map_element "name", to: :name
map_element "type", to: :type, namespace: "http://www.example.com", prefix: "ex1"
end
map_attribute "code", to: :code
end
end
class ComplexType < Lutaml::Model::Serializable
attribute :tag, AttributeValueType
attribute :content, :string
attribute :group, :string
import_model_attributes GroupOfItems
xml do
root "GroupOfItems"
map_attribute "tag", to: :tag
map_content to: :content
map_element :group, to: :group
import_model_mappings GroupOfItems
end
end
class SimpleType < Lutaml::Model::Serializable
import_model GroupOfItems
end
class GenericType < Lutaml::Model::Serializable
import_model_mappings GroupOfItems
end
<GroupOfItems xmlns:ex1="http://www.example.com">
<name>Name</name>
<ex1:type>Type</ex1:type>
</GroupOfItems>
> parsed = GroupOfItems.from_xml(xml)
> # Lutaml::Model::NoRootMappingError: "GroupOfItems has `no_root`, it allowed only for reusable models"
An attribute value transformation is used when the value of an attribute needs to be transformed around assignment.
There are occasions where the value of an attribute is to be transformed during assignment and retrieval, such that when the external usage of the value differs from the internal model representation.
Note
|
Value transformation can be applied at the attribute-level or at the serialization-mapping level. They can also be applied together. |
Given a model that stores a measurement composed of a numerical value and a unit, where the numerical value is used for calculations inside the model, but the external representation of that value is a string (across all serialization formats).
-
Internal:
number: 10.20
,unit: cm
. -
External:
"10.20 cm"
The transform
option at the attribute
method is used to define a
transformation Proc
for the attribute value.
Syntax:
class SomeObject < Lutaml::Model::Serializable
attribute :attribute_name, {attr_type}, transform: {
export: ->(value) { ... },
import: ->(value) { ... }
}
end
The transform
option also support collection attributes.
Where,
attribute_name
-
The name of the attribute.
attr_type
-
The type of the attribute.
transform
-
The option to define a transformation for the attribute value.
export
-
The transformation
Proc
for the value when it is being retrieved from the model. import
-
The transformation
Proc
for the value when it is being assigned to the model.
class Ceramic < Lutaml::Model::Serializable
attribute :name, :string, transform: {
export: ->(value) { value.upcase },
import: ->(value) { value.downcase }
}
end
> c = Ceramic.new(name: "Celadon")
> c.name
> # "CELADON"
> c.instance_attribute_get(:@name)
> # "Celadon"
> Ceramic.new(name: "Celadon").name = "Raku"
> # "RAKU"
An attribute can be defined as an enumeration by using the values
directive.
The values
directive is used to define acceptable values in an attribute. If
any other value is given, a Lutaml::Model::InvalidValueError
will be raised.
Syntax:
attribute :name_of_attribute, Type, values: [value1, value2, ...]
The values set inside the values:
option can be of any type, but they must
match the type of the attribute. The values are compared using the ==
operator,
so the type must implement the ==
method.
Also, If all the elements in values
directive are strings then lutaml-model
add some enum convenience methods, for each of the value the following three methods are added
-
value1
: will return value if set -
value1?
: will return true if value is set, false otherwise -
value1=
: will set the value ofname_of_attribute
equal tovalue1
if truthy value is given, and remove it otherwise.
values
directive to define acceptable values for an attribute (basic types)class GlazeTechnique < Lutaml::Model::Serializable
attribute :name, :string, values: ["Celadon", "Raku", "Majolica"]
end
> GlazeTechnique.new(name: "Celadon").name
> # "Celadon"
> GlazeTechnique.new(name: "Raku").name
> # "Raku"
> GlazeTechnique.new(name: "Majolica").name
> # "Majolica"
> GlazeTechnique.new(name: "Earthenware").name
> # Lutaml::Model::InvalidValueError: Invalid value for attribute 'name'
The values can be Serialize objects, which are compared using the ==
and the hash
methods through the Lutaml::Model::ComparableModel module.
values
directive to define acceptable values for an attribute (Serializable objects)class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :firing_temperature, :integer
end
class CeramicCollection < Lutaml::Model::Serializable
attribute :featured_piece,
Ceramic,
values: [
Ceramic.new(type: "Porcelain", firing_temperature: 1300),
Ceramic.new(type: "Stoneware", firing_temperature: 1200),
Ceramic.new(type: "Earthenware", firing_temperature: 1000),
]
end
> CeramicCollection.new(featured_piece: Ceramic.new(type: "Porcelain", firing_temperature: 1300)).featured_piece
> # Ceramic:0x0000000104ac7240 @type="Porcelain", @firing_temperature=1300
> CeramicCollection.new(featured_piece: Ceramic.new(type: "Bone China", firing_temperature: 1300)).featured_piece
> # Lutaml::Model::InvalidValueError: Invalid value for attribute 'featured_piece'
Serialize provides a validate
method that checks if all its attributes have
valid values. This is necessary for the case when a value is valid at the
component level, but not accepted at the aggregation level.
If a change has been made at the component level (a nested attribute has
changed), the aggregation level needs to call the validate
method to verify
acceptance of the newly updated component.
validate
method to check if all attributes have valid values> collection = CeramicCollection.new(featured_piece: Ceramic.new(type: "Porcelain", firing_temperature: 1300))
> collection.featured_piece.firing_temperature = 1400
> # No error raised in changed nested attribute
> collection.validate
> # Lutaml::Model::InvalidValueError: Invalid value for attribute 'featured_piece'
An attribute that accepts a string value accepts value validation using regular expressions.
Syntax:
attribute :name_of_attribute, :string, pattern: /regex/
pattern
option to restrict the value of an attributeIn this example, the color
attribute takes hex color values such as #ccddee
.
A regular expression can be used to validate values assigned to the attribute.
In this case, it is /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
.
class Glaze < Lutaml::Model::Serializable
attribute :color, :string, pattern: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/
end
> Glaze.new(color: '#ff0000').color
> # "#ff0000"
> Glaze.new(color: '#ff000').color
> # Lutaml::Model::InvalidValueError: Invalid value for attribute 'color'
Specify default values for attributes using the default
option.
The default
option can be set to a value or a lambda that returns a value.
Syntax:
attribute :name_of_attribute, Type, default: -> { value }
default
option to set a default value for an attributeclass Glaze < Lutaml::Model::Serializable
attribute :color, :string, default: -> { 'Clear' }
attribute :temperature, :integer, default: -> { 1050 }
end
> Glaze.new.color
> # "Clear"
> Glaze.new.temperature
> # 1050
The "default behavior" (pun intended) is to not render a default value if the current value is the same as the default value.
An attribute can be set to read the value as raw string for XML, by using the raw: true
option.
Syntax:
attribute :name_of_attribute, :string, raw: true
raw
option to read raw value for an XML attributeclass Person < Lutaml::Model::Serializable
attribute :name, :string
attribute :description, :string, raw: true
end
For the following XML snippet:
<Person>
<name>John Doe</name>
<description>
A <b>fictional person</b> commonly used as a <i>placeholder name</i>.
</description>
</Person>
> Person.from_xml(xml)
> # <Person:0x0000000107a3ca70
@description="\n A <b>fictional person</b> commonly used as a <i>placeholder name</i>.\n ",
@element_order=["text", "name", "text", "description", "text"],
@name="John Doe",
@ordered=nil>
Lutaml::Model allows you to translate a data model into serialization models of various serialization formats including XML, Hash, JSON, YAML, and TOML.
Depending on the serialization format, different methods are supported for defining serialization and deserialization mappings.
Serialization model mappings are defined under the xml
, hsh
, json
, yaml
,
and toml
blocks.
xml
, hsh
, json
, yaml
, toml
and key_value
blocks to define serialization mappingsclass Example < Lutaml::Model::Serializable
xml do
# ...
end
hsh do
# ...
end
json do
# ...
end
yaml do
# ...
end
toml do
# ...
end
key_value do
# ...
end
end
The root
method sets the root element tag name of the XML document.
If root
is not given, then the snake-cased class name will be used as the
root.
<example>
in XML <example>…</example>
.
Syntax:
xml do
root 'xml_element_name'
end
example
class Example < Lutaml::Model::Serializable
xml do
root 'example'
end
end
> Example.new.to_xml
> #<example></example>
The root element can be omitted by using the no_root
method.
When no_root
is used, only map_element
can be used because without a root
element there cannot be attributes.
Syntax:
xml do
no_root
end
class NameAndCode < Lutaml::Model::Serializable
attribute :name, :string
attribute :code, :string
xml do
no_root
map_element "code", to: :code
map_element "name", to: :name
end
end
<name>Name</name>
<code>ID-001</code>
> parsed = NameAndCode.from_xml(xml)
> # <NameAndCode:0x0000000107a3ca70 @code="ID-001", @name="Name">
> parsed.to_xml
> # <code>ID-001</code><name>Name</name>
The map_all
tag in XML mapping captures and maps all content within an XML
element into a single attribute in the target Ruby object.
The use case for map_all
is to tell Lutaml::Model to not parse the content of
the XML element at all, and instead handle it as an XML string.
Note
|
The corresponding method for key-value formats is at Mapping all key-value content. |
Warning
|
Notice that usage of mapping all will lead to incompatibility between serialization formats, i.e. the raw string content will not be portable as objects are across different formats. |
This is useful in the case where the content of an XML element is not to be handled by a Lutaml::Model::Serializable object.
This feature is commonly used with custom methods or a custom model object to handle the content.
This includes:
-
nested tags
-
attributes
-
text nodes
The map_all
tag is exclusive and cannot be combined with other mappings
(map_element
, map_content
) except for map_attribute
for the same element,
ensuring it captures the entire inner XML content.
Note
|
An error is raised if map_all is defined alongside any other mapping in
the same XML mapping context.
|
Syntax:
xml do
map_all to: :name_of_attribute
end
map_all
class ExampleMapping < Lutaml::Model::Serializable
attribute :description, :string
xml do
map_all to: :description
end
end
<ExampleMapping>Content with <b>tags</b> and <i>formatting</i>.</ExampleMapping>
> parsed = ExampleMapping.from_xml(xml)
> puts parsed.all_content
# "Content with <b>tags</b> and <i>formatting</i>."
The map_element
method maps an XML element to a data model attribute.
<name>
tag in <example><name>John Doe</name></example>
.
The value will be set to John Doe
.
Syntax:
xml do
map_element 'xml_element_name', to: :name_of_attribute
end
name
tag to the name
attributeclass Example < Lutaml::Model::Serializable
attribute :name, :string
xml do
root 'example'
map_element 'name', to: :name
end
end
<example><name>John Doe</name></example>
> Example.from_xml(xml)
> #<Example:0x0000000104ac7240 @name="John Doe">
> Example.new(name: "John Doe").to_xml
> #<example><name>John Doe</name></example>
If an element is mapped to a model object with the XML root
tag name set, the
mapped tag name will be used as the root name, overriding the root name.
class RecordDate < Lutaml::Model::Serializable
attribute :content, :string
xml do
root "recordDate"
map_content to: :content
end
end
class OriginInfo < Lutaml::Model::Serializable
attribute :date_issued, RecordDate, collection: true
xml do
root "originInfo"
map_element "dateIssued", to: :date_issued
end
end
> RecordDate.new(date: "2021-01-01").to_xml
> #<recordDate>2021-01-01</recordDate>
> OriginInfo.new(date_issued: [RecordDate.new(date: "2021-01-01")]).to_xml
> #<originInfo><dateIssued>2021-01-01</dateIssued></originInfo>
The map_attribute
method maps an XML attribute to a data model attribute.
Syntax:
xml do
map_attribute 'xml_attribute_name', to: :name_of_attribute
end
map_attribute
to map the value
attributeThe following class will parse the XML snippet below:
class Example < Lutaml::Model::Serializable
attribute :value, :integer
xml do
root 'example'
map_attribute 'value', to: :value
end
end
<example value="12"><name>John Doe</name></example>
> Example.from_xml(xml)
> #<Example:0x0000000104ac7240 @value=12>
> Example.new(value: 12).to_xml
> #<example value="12"></example>
The map_attribute
method does not inherit the root element’s namespace.
To specify a namespace for an attribute, please explicitly declare the
namespace and prefix in the map_attribute
method.
The following class will parse the XML snippet below:
class Attribute < Lutaml::Model::Serializable
attribute :value, :integer
xml do
root 'example'
map_attribute 'value', to: :value, namespace: "http://www.tech.co/XMI", prefix: "xl"
end
end
<example xl:value="20" xmlns:xl="http://www.tech.co/XMI"></example>
> Attribute.from_xml(xml)
> #<Attribute:0x0000000109436db8 @value=20>
> Attribute.new(value: 20).to_xml
> #<example xmlns:xl=\"http://www.tech.co/XMI\" xl:value=\"20\"/>
Content represents the text inside an XML element, inclusive of whitespace.
The map_content
method maps an XML element’s content to a data model
attribute.
Syntax:
xml do
map_content to: :name_of_attribute
end
map_content
to map content of the description
tagThe following class will parse the XML snippet below:
class Example < Lutaml::Model::Serializable
attribute :description, :string
xml do
root 'example'
map_content to: :description
end
end
<example>John Doe is my moniker.</example>
> Example.from_xml(xml)
> #<Example:0x0000000104ac7240 @description="John Doe is my moniker.">
> Example.new(description: "John Doe is my moniker.").to_xml
> #<example>John Doe is my moniker.</example>
CDATA is an XML feature that allows the inclusion of text that may contain characters that are unescaped in XML.
While CDATA is not preferred in XML, it is sometimes necessary to handle CDATA nodes for both input and output.
Note
|
The W3C XML Recommendation explicitly encourages escaping characters over usage of CDATA. |
Lutaml::Model supports the handling of CDATA nodes in XML in the following behavior:
-
When an attribute contains a CDATA node with no text:
-
On reading: The node (CDATA or text) is read as its value.
-
On writing: The value is written as its native type.
-
-
When an XML mapping sets
cdata: true
onmap_element
ormap_content
:-
On reading: The node (CDATA or text) is read as its value.
-
On writing: The value is written as a CDATA node.
-
-
When an XML mapping sets
cdata: false
onmap_element
ormap_content
:-
On reading: The node (CDATA or text) is read as its value.
-
On writing: The value is written as a text node (string).
-
Syntax:
xml do
map_content to: :name_of_attribute, cdata: (true | false)
map_element :name, to: :name, cdata: (true | false)
end
cdata
to map CDATA contentThe following class will parse the XML snippet below:
class Example < Lutaml::Model::Serializable
attribute :name, :string
attribute :description, :string
attribute :title, :string
attribute :note, :string
xml do
root 'example'
map_element :name, to: :name, cdata: true
map_content to: :description, cdata: true
map_element :title, to: :title, cdata: false
map_element :note, to: :note, cdata: false
end
end
<example><name><![CDATA[John]]></name><![CDATA[here is the description]]><title><![CDATA[Lutaml]]></title><note>Careful</note></example>
> Example.from_xml(xml)
> #<Example:0x0000000104ac7240 @name="John" @description="here is the description" @title="Lutaml" @note="Careful">
> Example.new(name: "John", description: "here is the description", title: "Lutaml", note: "Careful").to_xml
> #<example><name><![CDATA[John]]></name><![CDATA[here is the description]]><title>Lutaml</title><note>Careful</note></example>
The following class will parse the XML snippet below:
class Ceramic < Lutaml::Model::Serializable
attribute :name, :string
attribute :description, :string
attribute :temperature, :integer
xml do
root 'ceramic'
map_element 'name', to: :name
map_attribute 'temperature', to: :temperature
map_content to: :description
end
end
<ceramic temperature="1200"><name>Porcelain Vase</name> with celadon glaze.</ceramic>
> Ceramic.from_xml(xml)
> #<Ceramic:0x0000000104ac7240 @name="Porcelain Vase", @description=" with celadon glaze.", @temperature=1200>
> Ceramic.new(name: "Porcelain Vase", description: " with celadon glaze.", temperature: 1200).to_xml
> #<ceramic temperature="1200"><name>Porcelain Vase</name> with celadon glaze.</ceramic>
The namespace
method in the xml
block sets the namespace for the root
element.
Syntax:
xml do
namespace 'http://example.com/namespace'
end
xml do
namespace 'http://example.com/namespace', 'prefix'
end
namespace
method to set the namespace for the root elementclass Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, :string
xml do
root 'Ceramic'
namespace 'http://example.com/ceramic'
map_element 'Type', to: :type
map_element 'Glaze', to: :glaze
end
end
<Ceramic xmlns='http://example.com/ceramic'><Type>Porcelain</Type><Glaze>Clear</Glaze></Ceramic>
> Ceramic.from_xml(xml_file)
> #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear">
> Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
> #<Ceramic xmlns="http://example.com/ceramic"><Type>Porcelain</Type><Glaze>Clear</Glaze></Ceramic>
namespace
method to set a prefixed namespace for the root elementclass Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, :string
xml do
root 'Ceramic'
namespace 'http://example.com/ceramic', 'cer'
map_element 'Type', to: :type
map_element 'Glaze', to: :glaze
end
end
<cer:Ceramic xmlns='http://example.com/ceramic'><cer:Type>Porcelain</cer:Type><cer:Glaze>Clear</cer:Glaze></cer:Ceramic>
> Ceramic.from_xml(xml_file)
> #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear">
> Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
> #<cer:Ceramic xmlns="http://example.com/ceramic"><cer:Type>Porcelain</cer:Type><cer:Glaze>Clear</cer:Glaze></cer:Ceramic>
If the namespace is defined on a model attribute that already has a namespace, the mapped namespace will be given priority over the one defined in the class.
Syntax:
xml do
map_element 'xml_element_name', to: :name_of_attribute,
namespace: 'http://example.com/namespace',
prefix: 'prefix'
end
namespace
-
The XML namespace used by this element
prefix
-
The XML namespace prefix used by this element (optional)
namespace
option to set the namespace for an elementIn this example, glz
will be used for Glaze
if it is added inside the
Ceramic
class, and glaze
will be used otherwise.
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, Glaze
xml do
root 'Ceramic'
namespace 'http://example.com/ceramic'
map_element 'Type', to: :type
map_element 'Glaze', to: :glaze, namespace: 'http://example.com/glaze', prefix: "glz"
end
end
class Glaze < Lutaml::Model::Serializable
attribute :color, :string
attribute :temperature, :integer
xml do
root 'Glaze'
namespace 'http://example.com/old_glaze', 'glaze'
map_element 'color', to: :color
map_element 'temperature', to: :temperature
end
end
<Ceramic xmlns='http://example.com/ceramic'>
<Type>Porcelain</Type>
<glz:Glaze xmlns='http://example.com/glaze'>
<color>Clear</color>
<temperature>1050</temperature>
</glz:Glaze>
</Ceramic>
> # Using the original Glaze class namespace
> Glaze.new(color: "Clear", temperature: 1050).to_xml
> #<glaze:Glaze xmlns="http://example.com/old_glaze"><color>Clear</color><temperature>1050</temperature></glaze:Glaze>
> # Using the Ceramic class namespace for Glaze
> Ceramic.from_xml(xml_file)
> #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze=#<Glaze:0x0000000104ac7240 @color="Clear", @temperature=1050>>
> Ceramic.new(type: "Porcelain", glaze: Glaze.new(color: "Clear", temperature: 1050)).to_xml
> #<Ceramic xmlns="http://example.com/ceramic"><Type>Porcelain</Type><glz:Glaze xmlns="http://example.com/glaze"><color>Clear</color><temperature>1050</temperature></glz:Glaze></Ceramic>
The inherit
option is used at the element level to inherit the namespace from
the root element.
Syntax:
xml do
map_element 'xml_element_name', to: :name_of_attribute, namespace: :inherit
end
inherit
option to inherit the namespace from the root elementIn this example, the Type
element will inherit the namespace from the root.
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, :string
attribute :color, :string
xml do
root 'Ceramic'
namespace 'http://example.com/ceramic', 'cera'
map_element 'Type', to: :type, namespace: :inherit
map_element 'Glaze', to: :glaze
map_attribute 'color', to: :color, namespace: 'http://example.com/color', prefix: 'clr'
end
end
<cera:Ceramic
xmlns:cera='http://example.com/ceramic'
xmlns:clr='http://example.com/color'
clr:color="navy-blue">
<cera:Type>Porcelain</cera:Type>
<Glaze>Clear</Glaze>
</cera:Ceramic>
> Ceramic.from_xml(xml_file)
> #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear", @color="navy-blue">
> Ceramic.new(type: "Porcelain", glaze: "Clear", color: "navy-blue").to_xml
> #<cera:Ceramic xmlns:cera="http://example.com/ceramic"
# xmlns:clr='http://example.com/color'
# clr:color="navy-blue">
# <cera:Type>Porcelain</cera:Type>
# <Glaze>Clear</Glaze>
# </cera:Ceramic>
In XML there can be tags that contain content mixed with other tags and where whitespace is significant, such as to represent rich text.
<description><p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p></description>
To map this to Lutaml::Model we can use the mixed
option in either way:
-
when defining the model;
-
when referencing the model.
Note
|
This feature is not supported by Shale. |
To specify mixed content, the mixed: true
option needs to be set at the
xml
block’s root
method.
Syntax:
xml do
root 'xml_element_name', mixed: true
end
mixed
to treat root as mixed contentclass Paragraph < Lutaml::Model::Serializable
attribute :bold, :string, collection: true # allows multiple bold tags
attribute :italic, :string
xml do
root 'p', mixed: true
map_element 'bold', to: :bold
map_element 'i', to: :italic
end
end
> Paragraph.from_xml("<p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p>")
> #<Paragraph:0x0000000104ac7240 @bold="John Doe", @italic="28">
> Paragraph.new(bold: "John Doe", italic: "28").to_xml
> #<p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p>
ordered: true
maintains the order of XML Elements, while mixed: true
preserves the order of XML Elements and Content.
Note
|
When both options are used, mixed: true takes precedence.
|
To specify ordered content, the ordered: true
option needs to be set at the
xml
block’s root
method.
Syntax:
xml do
root 'xml_element_name', ordered: true
end
ordered
to treat root as ordered contentclass RootOrderedContent < Lutaml::Model::Serializable
attribute :bold, :string
attribute :italic, :string
attribute :underline, :string
xml do
root "RootOrderedContent", ordered: true
map_element :bold, to: :bold
map_element :italic, to: :italic
map_element :underline, to: :underline
end
end
<RootOrderedContent>
<underline>Moon</underline>
<italic>384,400 km</italic>
<bold>bell</bold>
</RootOrderedContent>
> instance = RootOrderedContent.from_xml(xml)
> #<RootOrderedContent:0x0000000104ac7240 @bold="bell", @italic="384,400 km", @underline="Moon">
> instance.to_xml
> #<RootOrderedContent><underline>Moon</underline><italic>384,400 km</italic><bold>bell</bold></RootOrderedContent>
Without Ordered True:
class RootOrderedContent < Lutaml::Model::Serializable
attribute :bold, :string
attribute :italic, :string
attribute :underline, :string
xml do
root "RootOrderedContent"
map_element :bold, to: :bold
map_element :italic, to: :italic
map_element :underline, to: :underline
end
end
<RootOrderedContent>
<underline>Moon</underline>
<italic>384,400 km</italic>
<bold>bell</bold>
</RootOrderedContent>
> instance = RootOrderedContent.from_xml(xml)
> #<RootOrderedContent:0x0000000104ac7240 @bold="bell", @italic="384,400 km", @underline="Moon">
> instance.to_xml
> #<RootOrderedContent>\n <bold>bell</bold>\n <italic>384,400 km</italic>\n <underline>Moon</underline>\n</RootOrderedContent>
The sequence
directive specifies that the defined attributes must appear in a
specified order in XML.
Note
|
Sequence only supports map_element mappings.
|
Syntax:
xml do
sequence do
map_element 'xml_element_name_1', to: :name_of_attribute_1
map_element 'xml_element_name_2', to: :name_of_attribute_2
# Add more map_element lines as needed to establish a complete sequence
end
end
The appearance of the elements in the XML document must match the order defined
in the sequence
block. In this case, the <xml_element_name_1>
element
should appear before the <xml_element_name_2>
element.
sequence
keyword to define a set of elements in desired order.class Kiln < Lutaml::Model::Serializable
attribute :id, :string
attribute :name, :string
attribute :type, :string
attribute :color, :string
xml do
sequence do
map_element :id, to: :id
map_element :name, to: :name
map_element :type, to: :type
map_element :color, to: :color
end
end
end
class KilnCollection < Lutaml::Model::Serializable
attribute :kiln, Kiln, collection: 1..2
xml do
root "collection"
map_element "kiln", to: :kiln
end
end
<collection>
<kiln>
<id>1</id>
<name>Nick</name>
<type>Hard</type>
<color>Black</color>
</kiln>
<kiln>
<id>2</id>
<name>John</name>
<type>Soft</type>
<color>White</color>
</kiln>
</collection>
> parsed = Kiln.from_xml(xml)
# => [
#<Kiln:0x0000000104ac7240 @id="1", @name="Nick", @type="Hard", @color="Black">,
#<Kiln:0x0000000104ac7240 @id="2", @name="John", @type="Soft", @color="White">
#]
> bad_xml = <<~HERE
<collection>
<kiln>
<name>Nick</name>
<id>1</id>
<color>Black</color>
<type>Hard</type>
</kiln>
</collection>
HERE
> parsed = Kiln.from_xml(bad_xml)
# => Lutaml::Model::ValidationError: Element 'name' is out of order in 'kiln' element
The
W3C "XMLSchema-instance"
namespace describes a number of attributes that can be used to control the
behavior of XML processors. One of these attributes is xsi:schemaLocation
.
The xsi:schemaLocation
attribute locates schemas for elements and attributes
that are in a specified namespace. Its value consists of pairs of a namespace
URI followed by a relative or absolute URL where the schema for that namespace
can be found.
Usage of xsi:schemaLocation
in an XML element depends on the declaration of
the XML namespace of xsi
, i.e.
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
. Without this namespace
LutaML will not be able to serialize the xsi:schemaLocation
attribute.
Note
|
It is most commonly attached to the root element but can appear further down the tree. |
The following snippet shows how xsi:schemaLocation
is used in an XML document:
<cera:Ceramic
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cera="http://example.com/ceramic"
xmlns:clr='http://example.com/color'
xsi:schemaLocation=
"http://example.com/ceramic http://example.com/ceramic.xsd
http://example.com/color http://example.com/color.xsd"
clr:color="navy-blue">
<cera:Type>Porcelain</cera:Type>
<Glaze>Clear</Glaze>
</cera:Ceramic>
LutaML::Model supports the xsi:schemaLocation
attribute in all XML
serializations by default, through the schema_location
attribute on the model
instance object.
xsi:schemaLocation
attribute in XML serializationIn this example, the xsi:schemaLocation
attribute will be automatically
supplied without the explicit need to define in the model, and allows for
round-trip serialization.
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, :string
attribute :color, :string
xml do
root 'Ceramic'
namespace 'http://example.com/ceramic', 'cera'
map_element 'Type', to: :type, namespace: :inherit
map_element 'Glaze', to: :glaze
map_attribute 'color', to: :color, namespace: 'http://example.com/color', prefix: 'clr'
end
end
xml_content = <<~HERE
<cera:Ceramic
xmlns:cera="http://example.com/ceramic"
xmlns:clr="http://example.com/color"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
clr:color="navy-blue"
xsi:schemaLocation="
http://example.com/ceramic http://example.com/ceramic.xsd
http://example.com/color http://example.com/color.xsd
">
<cera:Type>Porcelain</cera:Type>
<Glaze>Clear</Glaze>
</cera:Ceramic>
HERE
> c = Ceramic.from_xml(xml_content)
=>
#<Ceramic:0x00000001222bdd60
...
> schema_loc = c.schema_location
#<Lutaml::Model::SchemaLocation:0x0000000122773760
...
> schema_loc
=>
#<Lutaml::Model::SchemaLocation:0x0000000122773760
@namespace="http://www.w3.org/2001/XMLSchema-instance",
@original_schema_location="http://example.com/ceramic http://example.com/ceramic.xsd http://example.com/color http://example.com/color.xsd",
@prefix="xsi",
@schema_location=
[#<Lutaml::Model::Location:0x00000001222bd018 @location="http://example.com/ceramic.xsd", @namespace="http://example.com/ceramic">,
#<Lutaml::Model::Location:0x00000001222bcfc8 @location="http://example.com/color.xsd", @namespace="http://example.com/color">]>
> new_c = Ceramic.new(type: "Porcelain", glaze: "Clear", color: "navy-blue", schema_location: schema_loc).to_xml
> puts new_c
# <cera:Ceramic
# xmlns:cera="http://example.com/ceramic"
# xmlns:clr="http://example.com/color"
# xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
# clr:color="navy-blue"
# xsi:schemaLocation="
# http://example.com/ceramic http://example.com/ceramic.xsd
# http://example.com/color http://example.com/color.xsd
# ">
# <cera:Type>Porcelain</cera:Type>
# <cera:Glaze>Clear</cera:Glaze>
# </cera:Ceramic>
Note
|
For details on xsi:schemaLocation , please refer to the
W3C XML standard.
|
Lutaml::Model XML adapters use a default encoding of UTF-8
for both input and
output.
Serialization data to be parsed (deserialization) and serialization data to be exported (serialization) may be in a different character encoding than the default encoding used by the Lutaml::Model XML adapter. This mismatch may lead to incorrect data reading or incompatibilities when exporting data.
The possible values for setting character encoding to are:
-
A valid encoding value, e.g.
UTF-8
,Shift_JIS
,ASCII
; -
nil
to use the default encoding of the adapter. The behavior differs based on the adapter used.-
Nokogiri:
UTF-8
. The encoding is set to the default encoding of the Nokogiri library, which isUTF-8
. -
Oga:
UTF-8
. The encoding is set to the default encoding of the Oga library, which usesUTF-8
. -
Ox:
ASCII-8bit
. The encoding is set to the default encoding of the Ox library, which usesASCII-8bit
.
-
When the encoding
option is not set, the default encoding of UTF-8
is
used.
There are two ways to set the character encoding of the XML document during serialization:
- Instance setting
-
Setting the instance-level
encoding
option by settingModelClassInstance.encoding('…')
. This setting only affects serialization. - Per-export setting
-
Setting the
encoding
option when calling for serialization action using theModelClassInstance.to_xml(…, encoding: …)
method.
The encoding
value of an instance sets the character encoding of the XML
document during serialization.
Syntax:
ModelClassInstance.encoding = {encoding_value}
Where,
ModelClassInstance
-
An instance of the class that inherits from Lutaml::Model::Serializable.
{encoding_value}
-
The encoding of the output data.
class JapaneseCeramic < Lutaml::Model::Serializable
attribute :glaze_type, :string
attribute :description, :string
xml do
root 'JapaneseCeramic'
map_attribute 'glazeType', to: :glaze_type
map_element 'description' to: :description
end
end
# Create a new instance with UTF-8 data
> instance = JapaneseCeramic.new(glaze_type: "志野釉", description: "東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)")
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
# Set character encoding to Shift_JIS
> instance.encoding = "Shift_JIS"
#=> "Shift_JIS"
# Serialize the instance
> serialization_output = instance.to_xml
#=> #<JapaneseCeramic><glazeType>\x{5FD8}\x{91CE}\x{91C9}</glazeType><description>\x{6771}\x{4EAC}\x{56FD}\x{7ACB}\x{535A}\x{7269}\x{9928}\x{30B3}\x{30EC}\x{30AF}\x{30B7}\x{30E7}\x{30F3}\x{306E}\x{7BC0}\x{8336}\x{7897}\x{300C}\x{6A4B}\x{672C}\x{300D}\x{FF08}\x{6853}\x{5C71}\x{6642}\x{4EE3}\x{FF09}</description></JapaneseCeramic>
# Check character encoding of output
> serialization_output.encoding
#=> "Shift_JIS"
The encoding
option is used in the ModelClass#to_xml(…, encoding: …)
call to set the character encoding of the XML document during serialization.
The per-export encoding setting supersedes the instance-level encoding setting.
Syntax:
ModelClassInstance.to_xml(encoding: {encoding_value})
Where,
ModelClassInstance
-
An instance of the class that inherits from Lutaml::Model::Serializable.
{encoding_value}
-
The encoding of the output data.
The following class will parse the XML snippet below:
class Ceramic < Lutaml::Model::Serializable
attribute :potter, :string
attribute :description, :string
attribute :temperature, :integer
xml do
root 'ceramic'
map_element 'potter', to: :potter
map_content to: :description
end
end
<ceramic><potter>John & Jane</potter> A ∑ series of ∏ porcelain µ vases.</ceramic>
# Object with attributes
> ceramic_instance = Ceramic.new(potter: "John & Jane", description: " A ∑ series of ∏ porcelain µ vases.")
> #<Ceramic:0x0000000104ac7240 @potter="John & Jane", @description=" A ∑ series of ∏ porcelain µ vases.">
# Parsing the XML snippet with the default encoding of UTF-8
> ceramic_parsed = Ceramic.from_xml(xml)
> #<Ceramic:0x0000000104ac7242 @potter="John & Jane", @description=" A ∑ series of ∏ porcelain µ vases.">
# Object with attributes is equal to the parsed object
> ceramic_parsed == ceramic_instance
> # true
# Using the default encoding of UTF-8
> ceramic_instance.to_xml
> #<ceramic><potter>John & Jane</potter> A ∑ series of ∏ porcelain µ vases.</ceramic>
# Using the default encoding of the adapter, which is UTF-8 in this case
> ceramic_instance.to_xml(encoding: nil)
> #<ceramic><potter>John & Jane</potter> A ∑ series of ∏ porcelain µ vases.</ceramic>
# Using ASCII encoding
> ceramic_instance.to_xml(encoding: "ASCII")
> #<ceramic><potter>John & Jane</potter> A ∑ series of ∏ porcelain µ vases.</ceramic>
to_xml
overrides instance encodingclass JapaneseCeramic < Lutaml::Model::Serializable
attribute :glaze_type, :string
attribute :description, :string
xml do
root 'JapaneseCeramic'
map_attribute 'glazeType', to: :glaze_type
map_element 'description' to: :description
end
end
# Create a new instance with UTF-8 data
> instance = JapaneseCeramic.new(glaze_type: "志野釉", description: "東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)")
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
# Set character encoding to Shift_JIS
> instance.encoding = "Shift_JIS"
#=> "Shift_JIS"
# Serialize the instance
> serialization_output = instance.to_xml(encoding: "UTF-8")
#=> #<JapaneseCeramic><glazeType>志野釉</glazeType><description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description></JapaneseCeramic>
# Check character encoding of output
> serialization_output.encoding
#=> "UTF-8"
The character encoding of the XML document being parsed is specified using the
encoding
option when the ModelClass.from_{format}(…)
is called.
Syntax:
ModelClass.from_{format}(string_in_format, encoding: {encoding_value})
Where,
ModelClass
-
The class that inherits from Lutaml::Model::Serializable.
{format}
-
The format of the input data, e.g.
xml
,json
,yaml
,toml
. string_in_format
-
The input data in the specified format.
{encoding_value}
-
The encoding of the input data.
encoding
option during parsing data not encoded in the default encoding (UTF-8)Using the definition of JapaneseCeramic
at Instance setting.
This XML snippet is in Shift-JIS.
<JapaneseCeramic>
<glazeType>\x{5FD8}\x{91CE}\x{91C9}</glazeType>
<description>\x{6771}\x{4EAC}\x{56FD}\x{7ACB}\x{535A}\x{7269}\x{9928}\x{30B3}\x{30EC}\x{30AF}\x{30B7}\x{30E7}\x{30F3}\x{306E}\x{7BC0}\x{8336}\x{7897}\x{300C}\x{6A4B}\x{672C}\x{300D}\x{FF08}\x{6853}\x{5C71}\x{6642}\x{4EE3}\x{FF09}</description>
</JapaneseCeramic>
# Parse the XML snippet with the encoding of Shift_JIS
> instance = JapaneseCeramic.from_xml(xml, encoding: "Shift_JIS")
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
# Check character encoding of the instance
> instance.encoding
#=> "Shift_JIS"
# Serialize the instance using UTF-8
> serialization_output = instance.to_xml(encoding: "UTF-8")
#=> #<JapaneseCeramic><glazeType>志野釉</glazeType><description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description></JapaneseCeramic>
> serialization_output.encoding
#=> "UTF-8"
encoding
option is not set, the default encoding of the adapter is usedUsing the definition of JapaneseCeramic
at Instance setting.
This XML snippet is in UTF-8.
<JapaneseCeramic>
<glazeType>志野釉</glazeType>
<description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description>
</JapaneseCeramic>
In adapters that use a default encoding of UTF-8
, the content is parsed
properly.
> instance = JapaneseCeramic.from_xml(xml, encoding: nil)
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
> instance.encoding
#=> "UTF-8"
> serialization_output = instance.to_xml
#=> #<JapaneseCeramic><glazeType>志野釉</glazeType><description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description></JapaneseCeramic>
> serialization_output.encoding
#=> "UTF-8"
In adapters that use a default encoding of ASCII-8bit
, the content becomes
malformed.
> instance = JapaneseCeramic.from_xml(xml, encoding: nil)
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="�菑�", @description="�東京国立博物館コレクションの篠茶碗�橋本�桃山時代�">
> instance.encoding
#=> "ASCII-8bit"
> serialization_output = instance.to_xml
#=> #<JapaneseCeramic><glazeType>�菑�</glazeType><description>�東京国立博物館コレクションの篠茶碗�橋本�桃山時代�</description></JapaneseCeramic>
> serialization_output.encoding
#=> "ASCII-8bit"
Using the definition of JapaneseCeramic
at Instance setting.
This XML snippet is in UTF-8.
<JapaneseCeramic>
<glazeType>志野釉</glazeType>
<description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description>
</JapaneseCeramic>
> JapaneseCeramic.from_xml(xml, encoding: "Shift_JIS")
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="�菑���p���P", @description="�東京国立博物館コレクションの篠茶碗�橋本�桃山時代�">
Key-value data models like Hash, JSON, YAML, and TOML all share a similar structure where data is stored as key-value pairs.
Lutaml::Model
works with these formats in a similar way.
The map
method is used to define key-value mappings.
Syntax:
hsh | json | yaml | toml | key_value do
map 'key_value_model_attribute_name', to: :name_of_attribute
end
The key_value
method is a streamlined way to map all attributes for
serialization into key-value formats including Hash, JSON, YAML, and TOML.
If there is no definite differentiation between the key value formats, the
key_value
method simplifies defining mappings and improves code readability.
map
method to define the same mappings across all key-value formatsThis example shows how to define a key-value data model with the key_value
method which maps the same attributes across all key-value formats.
class CeramicModel < Lutaml::Model::Serializable
attribute :color, :string
attribute :glaze, :string
attribute :description, :string
key_value do
map :color, to: color
map :glz, to: :glaze
map :desc, to: :description
end
# Equivalent to the Hash, JSON, YAML, and TOML mappings.
#
# hsh and json and yaml and toml do
# map :id, to: color
# map :name, to: :full_name
# map :status, to: :current_status
# end
end
{
"color": "Navy Blue",
"glz": "Clear",
"desc": "A ceramic with a navy blue color and clear glaze."
}
color: Navy Blue
glz: Clear
desc: A ceramic with a navy blue color and clear glaze.
> CeramicModel.from_json(json)
> #<CeramicModel:0x0000000104ac7240 @color="Navy Blue", @glaze="Clear", @description="A ceramic with a navy blue color and clear glaze.">
> CeramicModel.new(color: "Navy Blue", glaze: "Clear", description: "A ceramic with a navy blue color and clear glaze.").to_json
> #{"color"=>"Navy Blue", "glz"=>"Clear", "desc"=>"A ceramic with a navy blue color and clear glaze."}
Specific key value formats can be mapping independently of other formats, including:
-
hsh
for the Hash format -
json
for the JSON format -
yaml
for the YAML format -
toml
for the TOML format
map
method to define key-value mappings per formatclass Example < Lutaml::Model::Serializable
attribute :name, :string
attribute :value, :integer
hsh do
map 'name', to: :name
map 'value', to: :value
end
json do
map 'name', to: :name
map 'value', to: :value
end
yaml do
map 'name', to: :name
map 'value', to: :value
end
toml do
map 'name', to: :name
map 'value', to: :value
end
end
{
"name": "John Doe",
"value": 28
}
> Example.from_json(json)
> #<Example:0x0000000104ac7240 @name="John Doe", @value=28>
> Example.new(name: "John Doe", value: 28).to_json
> #{"name"=>"John Doe", "value"=>28}
The map_all
tag captures and maps all content within a serialization format
into a single attribute in the target Ruby object.
The use case for map_all
is to tell Lutaml::Model to not parse the content at
all, and instead handle it as a raw string.
Note
|
The corresponding method for XML is at Mapping all XML content. |
Warning
|
Notice that usage of mapping all will lead to incompatibility between serialization formats, i.e. the raw string content will not be portable as objects are across different formats. |
This is useful when the content needs to be handled as-is without parsing into individual attributes.
The map_all
tag is exclusive and cannot be combined with other mappings,
ensuring it captures the entire content.
Note
|
An error is raised if map_all is defined alongside any other mapping in
the same mapping context.
|
Syntax:
hsh | json | yaml | toml | key_value do
map_all to: :name_of_attribute
end
map_all
to capture all content across different formatsclass Document < Lutaml::Model::Serializable
attribute :content, :string
hsh do
map_all to: :content
end
json do
map_all to: :content
end
yaml do
map_all to: :content
end
toml do
map_all to: :content
end
end
For JSON:
{
"sections": [
{ "title": "Introduction", "text": "Chapter 1" },
{ "title": "Conclusion", "text": "Final chapter" }
],
"metadata": {
"author": "John Doe",
"date": "2024-01-15"
}
}
For YAML:
sections:
- title: Introduction
text: Chapter 1
- title: Conclusion
text: Final chapter
metadata:
author: John Doe
date: 2024-01-15
The content is preserved exactly as provided:
> doc = Document.from_json(json_content)
> puts doc.content
> # "{\"sections\":[{\"title\":\"Introduction\",\"text\":\"Chapter 1\"},{\"title\":\"Conclusion\",\"text\":\"Final chapter\"}],\"metadata\":{\"author\":\"John Doe\",\"date\":\"2024-01-15\"}}"
> doc = Document.from_yaml(yaml_content)
> puts doc.content
> # "sections:\n - title: Introduction\n text: Chapter 1\n - title: Conclusion\n text: Final chapter\nmetadata:\n author: John Doe\n date: 2024-01-15\n"
The map
method can also be used to map nested key-value data models
by referring to a Lutaml::Model class as an attribute class.
class Glaze < Lutaml::Model::Serializable
attribute :color, :string
attribute :temperature, :integer
json do
map 'color', to: :color
map 'temperature', to: :temperature
end
end
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, Glaze
json do
map 'type', to: :type
map 'glaze', to: :glaze
end
end
{
"type": "Porcelain",
"glaze": {
"color": "Clear",
"temperature": 1050
}
}
> Ceramic.from_json(json)
> #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze=#<Glaze:0x0000000104ac7240 @color="Clear", @temperature=1050>>
> Ceramic.new(type: "Porcelain", glaze: Glaze.new(color: "Clear", temperature: 1050)).to_json
> #{"type"=>"Porcelain", "glaze"=>{"color"=>"Clear", "temperature"=>1050}}
Note
|
This feature is for key-value data model serialization and deserialization only. |
The map
method with the root_mappings
option is used for key-value data that
is keyed using an attribute value.
In other words, the key of a key-value pair in a collection is actually the value of an attribute that belongs to the value.
Simply put, the following two data structures are considered to have the same data:
id
attribute---
vase1:
name: Imperial Vase
bowl2:
name: 18th Century Bowl
id
attribute value located inside each element---
- id: vase1
name: Imperial Vase
- id: bowl2
name: 18th Century Bowl
There are key difference between these two data structures:
-
The keyed object (first data structure) ensures uniqueness of the
id
attribute value across the collection, while the array (second data structure) does not. -
The value of the
id
attribute in the first data structure exists outside of the formal structure of the data object, instead, it only exists at the collection level. On the other hand, the value exists inside the structure of the data object in the second data structure.
The map
method with the root_mappings
option, in practice, parses the first
data structure in the same way that you would access / manipulate the second
data structure, while retaining the serialization semantics of using an
attribute as key.
As a result, usage of lutaml-model across both types of collections are identical (except when serialized).
Syntax:
class SomeKeyedCollection < Lutaml::Model::Serializable
attribute :name_of_attribute, AttributeValueType, collection: true
hsh | json | yaml | toml | key_value do
map to: :name_of_attribute, (1)
root_mappings: { (2)
# `:key` is a reserved keyword
value_type_attribute_name_for_key: :key, (3)
# `:value` is a reserved keyword (and optional)
value_type_attribute_name_for_value: :value, (4)
# `[path name]` represents the path to access the value in the
# serialization data model to be assigned to
# `AttributeValueType.value_type_attribute_name_for_custom_type`
value_type_attribute_name_for_custom_type: [path name] (5)
}
end
end
class AttributeValueType < Lutaml::Model::Serializable
attribute :value_type_attribute_name_for_key, :string
attribute :value_type_attribute_name_for_value, :string
attribute :value_type_attribute_name_for_custom_type, CustomType
end
-
The
map
option indicates that this class represents the root of the serialization object being passed in. Thename_of_attribute
is the name of the attribute that will hold the collection data. (Mandatory) -
The
root_mappings
keyword specifies what the collection key represents and and value for model. (Mandatory) -
The
key
keyword specifies the attribute name of the individual collection object type that represents its key used in the collection. (Mandatory) -
The
value
keyword specifies the attribute name of the individual collection object type that represents its data used in the collection. (Optional, if not specified, the entire object is used as the value.) -
The
value_type_attribute_name_for_custom_type
is the name of the attribute inside the individual collection object (AttributeValueType
) that will hold the value accessible in the serialization data model fetched at[path name]
.
The mapping syntax here is similar to that of Attribute extraction except
that the :key
and :value
keywords are allowed in addition to {path}
.
There are 3 cases when working with a keyed collection:
-
Case 1: Only move the "key" into the collection object.
-
Case 2: Move the "key" into the collection object, override all other mappings. Maps
:key
and another attribute, then we override all the other mappings (clean slate) -
Case 3: Move the "key" into the collection object to an attribute, map the entire "value" to another attribute of the collection object.
In this case, the "key" of the keyed collection is moved into the collection object, and all other mappings are left as they are.
When the "key" is moved into the collection object, the following happens:
-
The "key" of the keyed collection maps to a particular attribute of the collection’s instance object.
-
The "value" of the keyed collection (with its various content) maps to the collection’s instance object following the collection’s instance object type’s default mappings.
The root_mappings
option should only contain one mapping, and the mapping
must lead to the :key
keyword.
Syntax:
class SomeKeyedCollection < Lutaml::Model::Serializable
attribute :name_of_attribute, AttributeValueType, collection: true
hsh | json | yaml | toml | key_value do
map to: :name_of_attribute,
root_mappings: {
value_type_attribute_name_for_key: :key, (1)
}
end
end
class AttributeValueType < Lutaml::Model::Serializable
attribute :value_type_attribute_name_for_key, :string
attribute :value_type_attribute_name_for_value, :string
attribute :value_type_attribute_name_for_custom_type, CustomType
end
-
The
:key
keyword specifies that the "key" of the keyed collection maps to thevalue_type_attribute_name_for_key
attribute of the collection’s instance object (i.e.AttributeValueType
).
map
with root_mappings
(only key
) to map a keyed collection into individual modelsGiven this data:
---
vase1:
name: Imperial Vase
bowl2:
name: 18th Century Bowl
A model can be defined for this YAML as follows:
# This is a normal Lutaml::Model class
class Ceramic < Lutaml::Model::Serializable
attribute :ceramic_id, :string
attribute :ceramic_name, :string
key_value do
map 'id', to: :ceramic_id
map 'name', to: :ceramic_name
end
end
# This is Lutaml::Model class that represents the collection of Ceramic objects
class CeramicCollection < Lutaml::Model::Serializable
attribute :ceramics, Ceramic, collection: true
key_value do
map to: :ceramics, # All data goes to the `ceramics` attribute
root_mappings: {
# The key of an object in this collection is mapped to the ceramic_id
# attribute of the Ceramic object.
ceramic_id: :key # "key" is a reserved keyword
}
end
end
# Parsing the YAML collection with dynamic data keys
> ceramic_collection = CeramicCollection.from_yaml(yaml)
> #<CeramicCollection:0x0000000104ac7240
@ceramics=
[#<Ceramic:0x0000000104ac6e30 @ceramic_id="vase1", @ceramic_name="Imperial Vase">,
#<Ceramic:0x0000000104ac58f0 @ceramic_id="bowl2", @ceramic_name="18th Century Bowl">]
# NOTE: When an individual Ceramic object is serialized, the `id` attribute is
# the original key in the incoming YAML data, and because there were no mappings defined along with the `:key`, everyting is mapped to the `Ceramic` object using the mappings defined in the `Ceramic` class.
> first_ceramic = ceramic_collection.ceramics.first
> puts first_ceramic.to_yaml
=>
# ---
# id: vase1
# name: Imperial Vase
# NOTE: When in a collection, the `ceramic_id` attribute is used to key the data,
# and it disappears from the individual object.
> puts ceramic_collection.to_yaml
=>
# ---
# vase1:
# name: Imperial Vase
# bowl2:
# name: 18th Century Bowl
# NOTE: When the collection is serialized, the `ceramic_id` attribute is used to
# key the data. This is defined through the `map` with `root_mappings` method in
# CeramicCollection.
> new_collection = CeramicCollection.new(ceramics: [
Ceramic.new(ceramic_id: "vase1", ceramic_name: "Imperial Vase"),
Ceramic.new(ceramic_id: "bowl2", ceramic_name: "18th Century Bowl")
])
> puts new_collection.to_yaml
=>
# ---
# vase1:
# name: Imperial Vase
# bowl2:
# name: 18th Century Bowl
In this use case, the "key" of the keyed collection is moved into the collection object, and all other mappings are overridden.
When more than one mapping rule exists in the root_mappings
option, the
root_mappings
option will override all other mappings in the collection object.
When the "key" is moved into the collection object, the following happens:
-
The "key" of the keyed collection maps to a particular attribute of the collection’s instance object.
-
The data of the "value" of the keyed collection have their own mappings overridden by the new mapping rules of the
root_mappings
option.
The root_mappings
option can contain more than one mapping, with one of
the mapping rules leading to the :key
keyword.
Syntax:
class SomeKeyedCollection < Lutaml::Model::Serializable
attribute :name_of_attribute, AttributeValueType, collection: true
hsh | json | yaml | toml | key_value do
map to: :name_of_attribute,
root_mappings: {
value_type_attribute_name_for_key: :key, (1)
value_type_attribute_name_for_value_data_1: "serialization_format_name_1", (2)
value_type_attribute_name_for_value_data_2: "serialization_format_name_2",
value_type_attribute_name_for_value_data_3: ["path name", ...] (3)
# ...
}
end
end
class AttributeValueType < Lutaml::Model::Serializable
attribute :value_type_attribute_name_for_key, :string
attribute :value_type_attribute_name_for_value_data_1, :string
attribute :value_type_attribute_name_for_value_data_2, SomeType
attribute :value_type_attribute_name_for_value_data_3, MoreType
# ...
end
-
The
:key
keyword specifies that the "key" of the keyed collection maps to thevalue_type_attribute_name_for_key
attribute of the collection’s instance object (i.e.AttributeValueType
). -
The
serialization_format_name_1
target specifies that theserialization_format_name_2
key of the keyed collection value maps to thevalue_type_attribute_name_for_value_data_1
attribute of the collection’s instance object. -
The
[path name]
target specifies to fetch from[path name]
in the serialization data model to be assigned to thevalue_type_attribute_name_for_value_data_3
attribute of the collection’s instance object.
When the root_mappings
mapping contains more than one mapping rule that is not
to :key
or :value
, the root_mappings
mapping will override all other
mappings in the collection object. This means that unmapped attributes in
root_mappings
will not be incorporated in the collection instance objects.
map
with root_mappings
(key
and complex value
) to map a keyed collection into individual models"vase1":
type: "vase"
details:
name: "Imperial Vase"
insignia: "Tang Tianbao"
urn:
primary: "urn:ceramic:vase:vase1"
"bowl2":
type: "bowl"
details:
name: "18th Century Bowl"
insignia: "Ming Wanli"
urn:
primary: "urn:ceramic:bowl:bowl2"
A model can be defined for this YAML as follows:
# This is a normal Lutaml::Model class
class CeramicDetails < Lutaml::Model::Serializable
attribute :name, :string
attribute :insignia, :string
key_value do
map 'name', to: :name
map 'insignia', to: :insignia
end
end
# This is a normal Lutaml::Model class
class Ceramic < Lutaml::Model::Serializable
attribute :ceramic_id, :string
attribute :ceramic_type, :string
attribute :ceramic_details, CeramicDetails
attribute :ceramic_urn, :string
key_value do
map 'id', to: :ceramic_id
map 'type', to: :ceramic_type
map 'details', to: :ceramic_details
map 'urn', to: :ceramic_urn
end
end
# This is Lutaml::Model class that represents the collection of Ceramic objects
class CeramicCollection < Lutaml::Model::Serializable
attribute :ceramics, Ceramic, collection: true
key_value do
map to: :ceramics, # All data goes to the `ceramics` attribute
root_mappings: {
# The key of an object in this collection is mapped to the ceramic_id
# attribute of the Ceramic object.
# (e.g. `vase1`, `bowl2`)
ceramic_id: :key,
ceramic_type: :type,
ceramic_details: "details",
ceramic_urn: ["urn", "primary"]
}
end
end
The output becomes:
> ceramics_collection = CeramicCollection.from_yaml(yaml)
=> #<CeramicCollection:0x0000000107a2cf30
@ceramics=
[#<Ceramic:0x0000000107a2cf30
@ceramic_id="vase1",
@ceramic_type="vase",
@ceramic_details=
#<CeramicDetails:0x0000000107a2cf30
@name="Imperial Vase",
@insignia="Tang Tianbao">,
@ceramic_urn="urn:ceramic:vase:vase1">,
#<Ceramic:0x0000000107a2cf30
@ceramic_id="bowl2",
@ceramic_type="bowl",
@ceramic_details=
#<CeramicDetails:0x0000000107a2cf30
@name="18th Century Bowl",
@insignia="Ming Wanli">
@ceramic_urn="urn:ceramic:bowl:bowl2">]
> first_ceramic = ceramics_collection.ceramics.first
> puts first_ceramic.to_yaml
=>
# ---
# id: vase1
# type: vase
# details:
# name: Imperial Vase
# insignia: Tang Tianbao
# urn: urn:ceramic:vase:vase1
> new_collection = CeramicCollection.new(ceramics: [
Ceramic.new(ceramic_id: "vase1",
ceramic_type: "vase",
ceramic_urn: "urn:ceramic:vase:vase1",
ceramic_details: CeramicDetails.new(
name: "Imperial Vase", insignia: "Tang Tianbao")
),
Ceramic.new(ceramic_id: "bowl2",
ceramic_type: "bowl",
ceramic_urn: "urn:ceramic:vase:bowl2",
ceramic_details: CeramicDetails.new(
name: "18th Century Bowl", insignia: "Ming Wanli")
)
])
> new_collection.to_yaml
>
# ---
# vase1:
# type: vase
# details:
# name: Imperial Vase
# insignia: Tang Tianbao
# urn:
# primary: urn:ceramic:vase:vase1
# bowl2:
# type: bowl
# details:
# name: 18th Century Bowl
# insignia: Ming Wanli
# urn:
# primary: urn:ceramic:bowl:bowl2
In this use case, the "key" of the keyed collection is moved into the collection object to an attribute, and the entire "value" of the keyed collection is mapped to another attribute of the collection object.
When the "key" is moved into the collection object, the following happens:
-
The "key" of the keyed collection maps to a particular attribute of the collection’s instance object.
-
The data of the "value" of the keyed collection will be entirely mapped into an attribute of the collection’s instance object.
-
The original mapping of the "value" attribute of the collection’s instance object is retained.
The root_mappings
option should only contain two mappings, and the mappings
must lead to both the :key
and :value
keywords.
Syntax:
class SomeKeyedCollection < Lutaml::Model::Serializable
attribute :name_of_attribute, AttributeValueType, collection: true
hsh | json | yaml | toml | key_value do
map to: :name_of_attribute,
root_mappings: {
value_type_attribute_name_for_key: :key, (1)
value_type_attribute_name_for_value: :value (2)
}
end
end
class AttributeValueType < Lutaml::Model::Serializable
attribute :value_type_attribute_name_for_key, :string
attribute :value_type_attribute_name_for_value, SomeObject
end
-
The
:key
keyword specifies that the "key" of the keyed collection maps to thevalue_type_attribute_name_for_key
attribute of the collection’s instance object (i.e.AttributeValueType
). -
The
:value
keyword specifies that the entire "value" of the keyed collection maps to thevalue_type_attribute_name_for_value
attribute of the collection’s instance object (i.e.SomeObject
).
When the root_mappings
mapping contains more than one mapping rule, the
root_mappings
mapping will override all other mappings in the collection
object. This means that unmapped attributes in root_mappings
will not be
incorporated in the collection instance objects.
map
with root_mappings
(key
and value
) to map a keyed collection into individual modelsGiven this data:
---
vase1:
name: Imperial Vase
insignia: "Tang Tianbao"
bowl2:
name: 18th Century Bowl
insignia: "Ming Wanli"
A model can be defined for this YAML as follows:
# This is a normal Lutaml::Model class
class CeramicDetails < Lutaml::Model::Serializable
attribute :name, :string
attribute :insignia, :string
key_value do
map 'name', to: :name
map 'insignia', to: :insignia
end
end
# This is a normal Lutaml::Model class
class Ceramic < Lutaml::Model::Serializable
attribute :ceramic_id, :string
attribute :ceramic_details, CeramicDetails
key_value do
map 'id', to: :ceramic_id
map 'details', to: :ceramic_details
end
end
# This is Lutaml::Model class that represents the collection of Ceramic objects
class CeramicCollection < Lutaml::Model::Serializable
attribute :ceramics, Ceramic, collection: true
key_value do
map to: :ceramics, # All data goes to the `ceramics` attribute
root_mappings: {
# The key of an object in this collection is mapped to the ceramic_id
# attribute of the Ceramic object.
# (e.g. `vase1`, `bowl2`)
ceramic_id: :key,
# The value of an object in this collection is mapped to the
# ceramic_details attribute of the Ceramic object.
# (e.g. `name: 18th Century Bowl`, `insignia: "Ming Wanli"`
ceramic_details: :value
}
end
end
# Parsing the YAML collection with dynamic data keys
> ceramic_collection = CeramicCollection.from_yaml(yaml)
> #<CeramicCollection:0x0000000104ac7240
@ceramics=
[#<Ceramic:0x0000000104ac6e30
@ceramic_id="vase1",
@ceramic_details=
#<CeramicDetails:0x0000000104ac6e30
@name="Imperial Vase",
@insignia="Tang Tianbao">,
#<Ceramic:0x0000000104ac58f0
@ceramic_id="bowl2",
@ceramic_details=
#<CeramicDetails:0x0000000104ac58f0
@name="18th Century Bowl",
@insignia="Ming Wanli">]
# NOTE: When an individual Ceramic object is serialized, the `id` attribute is
# the original key in the incoming YAML data.
> first_ceramic = ceramic_collection.ceramics.first
> puts first_ceramic.to_yaml
=>
# ---
# id: vase1
# details:
# name: Imperial Vase
# insignia: Tang Tianbao
# NOTE: When in a collection, the `ceramic_id` attribute is used to key the data,
# and it disappears from the individual object.
> puts ceramic_collection.to_yaml
=>
# ---
# vase1:
# name: Imperial Vase
# insignia: Tang Tianbao
# bowl2:
# name: 18th Century Bowl
# insignia: Ming Wanli
# NOTE: When the collection is serialized, the `ceramic_id` attribute is used to
# key the data. This is defined through the `map` with `root_mappings` method in
# CeramicCollection.
> new_collection = CeramicCollection.new(ceramics: [
Ceramic.new(ceramic_id: "vase1",
ceramic_details: CeramicDetails.new(
name: "Imperial Vase", insignia: "Tang Tianbao")
),
Ceramic.new(ceramic_id: "bowl2",
ceramic_details: CeramicDetails.new(
name: "18th Century Bowl", insignia: "Ming Wanli")
)
])
> puts new_collection.to_yaml
=>
# ---
# vase1:
# name: Imperial Vase
# insignia: Tang Tianbao
# bowl2:
# name: 18th Century Bowl
# insignia: Ming Wanli
Note
|
This feature is for key-value data model serialization only. |
The child_mappings
option is used to extract results from a key-value
serialization data model (Hash, JSON, YAML, TOML) into a Lutaml::Model::Serializable
object (collection or not).
The values are extracted from the key-value data model using the list of keys provided.
Syntax:
class SomeObject < Lutaml::Model::Serializable
attribute :name_of_attribute, AttributeValueType, collection: true
hsh | json | yaml | toml | key_value do
map 'key_value_model_attribute_name', to: :name_of_attribute,
child_mappings: {
value_type_attribute_name_1: (1)
{path_to_value_1}, (2)
value_type_attribute_name_2:
{path_to_value_2},
# ...
}
end
end
-
The
value_type_attribute_name_1
is the attribute name in theAttributeValueType
model. The value of this attribute will be assigned the key of the hash in the key-value data model. -
The
path_to_value_1
is an array of keys that represent the path to the value in the key-value serialization data model. The keys are used to extract the value from the key-value serialization data model and assign it to the attribute in theAttributeValueType
model.The
path_to_value
is in a nested array format with each value a symbol or a string, where each symbol represents a key to traverse down. The last key in the path is the value to be extracted.
The following JSON contains 2 keys in schema named engine
and gearbox
.
{
"components": {
"engine": {
"manufacturer": "Ford",
"model": "V8"
},
"gearbox": {
"manufacturer": "Toyota",
"model": "4-speed"
}
}
}
The path to value for the engine
schema is [:components, :engine]
and for
the gearbox
schema is [:components, :gearbox]
.
In path_to_value
, the :key
and :value
are reserved instructions used to
assign the key or value of the serialization data respectively as the value to
the attribute.
In the following JSON content, the path_to_value
for the object keys named
engine
and gearbox
will utilize the :key
keyword to assign the key of the
object as the value of a designated attribute.
{
"components": {
"engine": { /*...*/ },
"gearbox": { /*...*/ }
}
}
If a specified value path is not found, the corresponding attribute in the model
will be assigned a nil
value.
nil
when the path_to_value
is not foundIn the following JSON content, the path_to_value
of [:extras, :sunroof]
and
[:extras, :drinks_cooler]
at the object "gearbox"
would be set to nil
.
{
"components": {
"engine": {
"manufacturer": "Ford",
"extras": {
"sunroof": true,
"drinks_cooler": true
}
},
"gearbox": {
"manufacturer": "Toyota"
}
}
}
child_mappings
option to extract values from a key-value data modelThe following JSON contains 2 keys in schema named foo
and bar
.
{
"schemas": {
"foo": { (1)
"path": { (2)
"link": "link one",
"name": "one"
}
},
"bar": { (1)
"path": { (2)
"link": "link two",
"name": "two"
}
}
}
}
-
The keys
foo
andbar
are to be mapped to theid
attribute. -
The nested
path.link
andpath.name
keys are used as thelink
andname
attributes, respectively.
A model can be defined for this JSON as follows:
class Schema < Lutaml::Model::Serializable
attribute :id, :string
attribute :link, :string
attribute :name, :string
end
class ChildMappingClass < Lutaml::Model::Serializable
attribute :schemas, Schema, collection: true
json do
map "schemas", to: :schemas,
child_mappings: {
id: :key,
link: %i[path link],
name: %i[path name],
}
end
end
The output becomes:
> ChildMappingClass.from_json(json)
> #<ChildMappingClass:0x0000000104ac7240
@schemas=
[#<Schema:0x0000000104ac6e30 @id="foo", @link="link one", @name="one">,
#<Schema:0x0000000104ac58f0 @id="bar", @link="link two", @name="two">]>
> ChildMappingClass.new(schemas: [Schema.new(id: "foo", link: "link one", name: "one"), Schema.new(id: "bar", link: "link two", name: "two")]).to_json
> #{"schemas"=>{"foo"=>{"path"=>{"link"=>"link one", "name"=>"one"}}, {"bar"=>{"path"=>{"link"=>"link two", "name"=>"two"}}}}}
In this example:
-
The
key
of each schema (foo
andbar
) is mapped to theid
attribute. -
The nested
path.link
andpath.name
keys are mapped to thelink
andname
attributes, respectively.
A mapping value transformation is used when the value of an attribute needs to be transformed around the serialization process. Collection attributes are also supported.
This is useful when the representation of the value in a serialization format differs from its internal representation in the model.
Note
|
Value transformation can be applied at the attribute-level or at the serialization-mapping level. They can also be applied together. |
Syntax:
class SomeObject < Lutaml::Model::Serializable
# Attribute-level transformation
attribute :attribute_name, {attr_type}, transform: { (1)
export: ->(value) { ... },
import: ->(value) { ... }
}
# Mapping-level transformation in JSON format
{key_value_formats} do
map "key", to: :attribute_name, transform: { (2)
export: ->(value) { ... },
import: ->(value) { ... }
}
end
# Mapping-level transformation in XML format
xml do
map_element "ElementName", to: :attribute_name, transform: { (3)
export: ->(value) { ... },
import: ->(value) { ... }
}
map_attribute "AttributeName", to: :attribute_name, transform: {
export: ->(value) { ... },
import: ->(value) { ... }
}
end
end
-
At the attribute level, the
transform
option applied to theattribute
method is used to define the transformation for the attribute. -
At the mapping level (for
{key_value_formats}
formats), thetransform
option applied to themap
method is used to define the transformation for the mapping. -
At the mapping level (for the XML format), the
transform
option applied to themap_*
methods is used to define the transformation for the mapping.
Where,
attribute_name
-
The name of the attribute.
attr_type
-
The type of the attribute.
- Attribute-level
transform
-
The option to define a transformation for the attribute value.
- Attribute-level
export
-
The transformation
Proc
for the value when it is being retrieved from the model. - Attribute-level
import
-
The transformation
Proc
for the value when it is being assigned to the model. {key_value_formats}
-
The serialization format (e.g.
hsh
,json
,yaml
,toml
,key_value
) for which the mapping is defined. - Mapping-level
transform
-
The option to define a transformation for the serialization mapping value. The value given to the Proc is the model attribute value that does not go through attribute-level transform.
- Mapping-level
export
-
The transformation
Proc
for the attribute value when it is being written to the serialization format. - Mapping-level
import
-
The transformation
Proc
for the value when it is being read from the serialization format and assigned to the model.
class Ceramic < Lutaml::Model::Serializable
attribute :glaze_type, :string
# Mapping-level transformation in key-value formats
json do
map "glazeType", to: :glaze_type, transform: {
export: ->(value) { "Traditional #{value}" },
import: ->(value) { value.gsub("Traditional ", "") }
}
end
# Mapping-level transformation in XML format
xml do
root "Ceramic"
map_attribute "glaze-type", to: :glaze_type, transform: {
export: ->(value) { "Traditional #{value}" },
import: ->(value) { value.gsub("Traditional ", "") }
}
end
end
ceramic = Ceramic.new(glaze_type: "celadon")
# Export transformation applied on defined mapping
ceramic.to_json
# => {"glazeType": "Traditional celadon"}
# Export transformation applied on defined mapping
ceramic.to_xml
# => <Ceramic glaze-type="Traditional celadon"/>
# No export transformation when no mapping exists
ceramic.to_yaml
# => glaze_type: "celadon"
# Import transformation applied on defined mapping
ceramic = Ceramic.from_json('{ "glazeType" => "Traditional celadon" }')
ceramic.glaze_type
# => "celadon"
# Import transformation applied on defined mapping
ceramic = Ceramic.from_xml('<Ceramic glaze-type="Traditional raku"/>')
ceramic.glaze_type
# => "raku"
# No import transformation when no mapping exists
ceramic = Ceramic.from_yaml('glaze_type: "Traditional celadon"')
ceramic.glaze_type
# => "Traditional celadon"
Attribute-level and mapping-level transformations can be used together for the same attribute in a chained fashion.
Precedence applies to the two levels of transformation for deserialization:
-
Mapping-level transformation, if defined, occurs first
-
Attribute-level transformation, if defined, is applied to the result of the mapping-level transformation
Conversely, precedence applies in the same order for serialization:
-
Attribute-level transformation, if defined, occurs first
-
Mapping-level transformation, if defined, is applied to the result of the attribute-level transformation
This mechanism allows for flexible value transformations without needing format-specific custom methods.
╔════════════════════════════╗ ╔════════════════════════════╗
║ Serialization Format Value ║ ║ Serialization Format Value ║
╚════════════════════════════╝ ╚════════════════════════════╝
| ▲
▼ |
╔════════════════════════════╗ ╔════════════════════════════╗
║ Mapping Transform ║ ║ Mapping Transform ║
╚════════════════════════════╝ ╚════════════════════════════╝
| ▲
▼ |
╔════════════════════════════╗ ╔════════════════════════════╗
║ Attribute Transform ║ ║ Attribute Transform ║
╚════════════════════════════╝ ╚════════════════════════════╝
| ▲
▼ |
╔════════════════════════════╗ ╔════════════════════════════╗
║ Model Attribute Value ║ ║ Model Attribute Value ║
╚════════════════════════════╝ ╚════════════════════════════╝
class Ceramic < Lutaml::Model::Serializable
# Attribute-level transformation
attribute :glaze_type, :string, transform: {
export: ->(value) { "Ceramic #{value}" },
import: ->(value) { value.gsub("Ceramic ", "") }
}
# Mapping-level transformation in key-value formats
json do
map "glazeType", to: :glaze_type, transform: {
export: ->(value) { "Traditional #{value}" },
import: ->(value) { value.gsub("Traditional ", "") }
}
end
# Mapping-level transformation in XML format
xml do
root "Ceramic"
map_attribute "glaze-type", to: :glaze_type, transform: {
export: ->(value) { "Traditional #{value}" },
import: ->(value) { value.gsub("Traditional ", "") }
}
end
end
ceramic = Ceramic.new(glaze_type: "Ceramic celadon")
# Attribute-level export transformation applied
ceramic.glaze_type
# => "Ceramic celadon"
# Internal representation
ceramic.instance_value_get(:@glaze_type)
# => "celadon"
# Mapping-level export transformation applied to attribute-level transformed value
ceramic.to_json
# => {"glazeType": "Traditional Ceramic celadon"}
# Mapping-level export transformation applied to attribute-level transformed value
ceramic.to_xml
# => <Ceramic glaze-type="Traditional Ceramic celadon"/>
# No mapping-level export transformation when no mapping exists
ceramic.to_yaml
# => glaze_type: "Ceramic celadon"
# Attribute-level import transformation applied to mapping-level transformed value
ceramic = Ceramic.from_json('{ "glazeType" => "Traditional Ceramic celadon" }')
ceramic.glaze_type
# => "Ceramic celadon"
# Attribute-level import transformation applied to mapping-level transformed value
ceramic = Ceramic.from_xml('<Ceramic glaze-type="Traditional Ceramic raku"/>')
ceramic.glaze_type
# => "Ceramic raku"
# No mapping-level import transformation when no mapping exists
ceramic = Ceramic.from_yaml('glaze_type: "Ceramic celadon"')
ceramic.glaze_type
# => "Ceramic celadon"
The Serialize
module can be used to define only serialization mappings for a
separately defined data model class (a Ruby class).
Note
|
This is traditionally called "custom model". |
Syntax:
class MappingClass < Lutaml::Model::Serializable
model {DataModelClass}
# ...
end
Where,
MappingClass
-
The class that represents the serialization mappings. This class must be a subclass of
Lutaml::Model::Serializable
. DataModelClass
-
The class that represents the data model.
When using a separate data model class, it is important to remember that the
serialization methods (instance#to_*
, klass.from_*
, such as
instance.to_yaml
, instance.to_xml
or Klass.from_yaml
, Klass.from_xml
),
are to be called on the mapping class, not the data model instance.
model
method to define serialization mappings for a separate modelclass Ceramic
attr_accessor :type, :glaze
def name
"#{type} with #{glaze}"
end
end
class CeramicSerialization < Lutaml::Model::Serializable
model Ceramic
xml do
map_element 'type', to: :type
map_element 'glaze', to: :glaze
end
end
> Ceramic.new(type: "Porcelain", glaze: "Clear").name
> # "Porcelain with Clear"
> CeramicSerialization.from_xml(xml)
> #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear">
> Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
> #<Ceramic><type>Porcelain</type><glaze>Clear</glaze></Ceramic>
model
method to define serialization mappings for a separate model in a model hierarchyThe following class will parse the XML snippet below:
class CustomModelChild
attr_accessor :street, :city
end
class CustomModelChildMapper < Lutaml::Model::Serializable
model CustomModelChild
attribute :street, Lutaml::Model::Type::String
attribute :city, Lutaml::Model::Type::String
xml do
map_element :street, to: :street
map_element :city, to: :city
end
end
class CustomModelParentMapper < Lutaml::Model::Serializable
attribute :first_name, Lutaml::Model::Type::String
attribute :child_mapper, CustomModelChildMapper
xml do
map_element :first_name, to: :first_name
map_element :CustomModelChild,
with: { to: :child_to_xml, from: :child_from_xml }
end
def child_to_xml(model, parent, doc)
child_el = doc.create_element("CustomModelChild")
street_el = doc.create_element("street")
city_el = doc.create_element("city")
doc.add_text(street_el, model.child_mapper.street)
doc.add_text(city_el, model.child_mapper.city)
doc.add_element(child_el, street_el)
doc.add_element(child_el, city_el)
doc.add_element(parent, child_el)
end
def child_from_xml(model, value)
model.child_mapper ||= CustomModelChild.new
model.child_mapper.street = value["elements"]["street"].text
model.child_mapper.city = value["elements"]["city"].text
end
end
<CustomModelParent>
<first_name>John</first_name>
<CustomModelChild>
<street>Oxford Street</street>
<city>London</city>
</CustomModelChild>
</CustomModelParent>
> instance = CustomModelParentMapper.from_xml(xml)
> #<CustomModelParent:0x0000000107c9ca68 @child_mapper=#<CustomModelChild:0x0000000107c95218 @city="London", @street="Oxford Street">, @first_name="John">
> CustomModelParentMapper.to_xml(instance)
> #<CustomModelParent><first_name>John</first_name><CustomModelChild><street>Oxford Street</street><city>London</city></CustomModelChild></CustomModelParent>
By default, attributes with default values are not rendered if the current value is the same as the default value.
In certain cases, it is necessary to render the default value even if the
current value is the same as the default value. This is achieved by setting the
render_default
option to true
.
Syntax:
attribute :name_of_attribute, Type, default: -> { value }
xml do
map_element 'name_of_attribute', to: :name_of_attribute, render_default: true
map_attribute 'name_of_attribute', to: :name_of_attribute, render_default: true
end
hsh | json | yaml | toml | key_value do
map 'name_of_attribute', to: :name_of_attribute, render_default: true
end
render_default
option to force encoding the default valueclass Glaze < Lutaml::Model::Serializable
attribute :color, :string, default: -> { 'Clear' }
attribute :opacity, :string, default: -> { 'Opaque' }
attribute :temperature, :integer, default: -> { 1050 }
attribute :firing_time, :integer, default: -> { 60 }
xml do
root "glaze"
map_element 'color', to: :color
map_element 'opacity', to: :opacity, render_default: true
map_attribute 'temperature', to: :temperature
map_attribute 'firingTime', to: :firing_time, render_default: true
end
json do
map 'color', to: :color
map 'opacity', to: :opacity, render_default: true
map 'temperature', to: :temperature
map 'firingTime', to: :firing_time, render_default: true
end
end
render_default: true
are rendered when the value is identical to the default> glaze_new = Glaze.new
> puts glaze_new.to_xml
# <glaze firingTime="60">
# <opacity>Opaque</opacity>
# </glaze>
> puts glaze_new.to_json
# {"firingTime":60,"opacity":"Opaque"}
render_default: true
with non-default values are rendered> glaze = Glaze.new(color: 'Celadon', opacity: 'Semitransparent', temperature: 1300, firing_time: 90)
> puts glaze.to_xml
# <glaze color="Celadon" temperature="1300" firingTime="90">
# <opacity>Semitransparent</opacity>
# </glaze>
> puts glaze.to_json
# {"color":"Celadon","temperature":1300,"firingTime":90,"opacity":"Semitransparent"}
The mapping methods support multiple names mapping to a single attribute using an array of names.
Syntax:
hsh | json | yaml | toml | key_value do
map ["name1", "name2"], to: :attribute_name
end
xml do
map_element ["name1", "name2"], to: :attribute_name
map_attribute ["attr1", "attr2"], to: :attribute_name
end
When serializing, the first element in the array of mapped names is always used as the output name.
class CustomModel < Lutaml::Model::Serializable
attribute :full_name, Lutaml::Model::Type::String
attribute :color, Lutaml::Model::Type::String
attribute :id, Lutaml::Model::Type::String
json do
map ["name", "custom_name"], with: { to: :name_to_json, from: :name_from_json }
map ["color", "shade"], with: { to: :color_to_json, from: :color_from_json }
end
xml do
root "CustomModel"
map_element ["name", "custom-name"], with: { to: :name_to_xml, from: :name_from_xml }
map_element ["color", "shade"], with: { to: :color_to_xml, from: :color_from_xml }
map_attribute ["id", "identifier"], to: :id
end
# Custom methods for JSON
def name_to_json(model, doc)
doc["name"] = "JSON Model: #{model.full_name}"
end
def name_from_json(model, value)
model.full_name = value&.sub(/^JSON Model: /, "")
end
def color_to_json(model, doc)
doc["color"] = model.color.upcase
end
def color_from_json(model, value)
model.color = value&.downcase
end
# Custom methods for XML
def name_to_xml(model, parent, doc)
el = doc.create_element("name")
doc.add_text(el, "XML Model: #{model.full_name}")
doc.add_element(parent, el)
end
def name_from_xml(model, value)
model.full_name = value.sub(/^XML Model: /, "")
end
def color_to_xml(model, parent, doc)
el = doc.create_element("color")
doc.add_text(el, model.color.upcase)
doc.add_element(parent, el)
end
def color_from_xml(model, value)
model.color = value.downcase
end
end
For JSON:
{
"custom_name": "JSON Model: Vase",
"shade": "BLUE",
"identifier": "123"
}
For XML:
<CustomModel id="123">
<name>XML Model: Vase</name>
<color>BLUE</color>
</CustomModel>
> model = CustomModel.from_json(json)
> model.full_name
> # "Vase"
> model.color
> # "blue"
Delegate attribute mappings to nested objects using the delegate
option.
Syntax:
xml | hsh | json | yaml | toml do
map 'key_value_model_attribute_name', to: :name_of_attribute, delegate: :model_to_delegate_to
end
delegate
option to map attributes to nested objectsThe following class will parse the JSON snippet below:
class Glaze < Lutaml::Model::Serializable
attribute :color, :string
attribute :temperature, :integer
json do
map 'color', to: :color
map 'temperature', to: :temperature
end
end
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, Glaze
json do
map 'type', to: :type
map 'color', to: :color, delegate: :glaze
end
end
{
"type": "Porcelain",
"color": "Clear"
}
> Ceramic.from_json(json)
> #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze=#<Glaze:0x0000000104ac7240 @color="Clear", @temperature=nil>>
> Ceramic.new(type: "Porcelain", glaze: Glaze.new(color: "Clear")).to_json
> #{"type"=>"Porcelain", "color"=>"Clear"}
Note
|
The corresponding keyword used by Shale is receiver: instead of
delegate: .
|
Define custom methods for specific attribute mappings using the with:
key for
each serialization mapping block for from
and to
.
Syntax:
xml do
map_element 'element_name', to: :name_of_element, with: {
to: :method_name_to_serialize,
from: :method_name_to_deserialize
}
map_attribute 'attribute_name', to: :name_of_attribute, with: {
to: :method_name_to_serialize,
from: :method_name_to_deserialize
}
map_content, to: :name_of_content, with: {
to: :method_name_to_serialize,
from: :method_name_to_deserialize
}
end
with:
key to define custom serialization methods for XMLThe following class will parse the XML snippet below:
class Metadata < Lutaml::Model::Serializable
attribute :category, :string
attribute :identifier, :string
end
class CustomCeramic < Lutaml::Model::Serializable
attribute :name, :string
attribute :size, :integer
attribute :description, :string
attribute :metadata, Metadata
xml do
map_element "Name", to: :name, with: { to: :name_to_xml, from: :name_from_xml }
map_attribute "Size", to: :size, with: { to: :size_to_xml, from: :size_from_xml }
map_content with: { to: :description_to_xml, from: :description_from_xml }
map_element :metadata, to: :metadata, with: { to: :metadata_to_xml, from: :metadata_from_xml }
end
def name_to_xml(model, parent, doc)
el = doc.create_element("Name")
doc.add_text(el, "XML Masterpiece: #{model.name}")
doc.add_element(parent, el)
end
def name_from_xml(model, value)
model.name = value.sub(/^XML Masterpiece: /, "")
end
def size_to_xml(model, parent, doc)
doc.add_attribute(parent, "Size", model.size + 3)
end
def size_from_xml(model, value)
model.size = value.to_i - 3
end
def description_to_xml(model, parent, doc)
doc.add_text(parent, "XML Description: #{model.description}")
end
def description_from_xml(model, value)
model.description = value.join.strip.sub(/^XML Description: /, "")
end
def metadata_to_xml(model, parent, doc)
metadata_el = doc.create_element("metadata")
category_el = doc.create_element("category")
identifier_el = doc.create_element("identifier")
doc.add_text(category_el, model.metadata.category)
doc.add_text(identifier_el, model.metadata.identifier)
doc.add_element(metadata_el, category_el)
doc.add_element(metadata_el, identifier_el)
doc.add_element(parent, metadata_el)
end
def metadata_from_xml(model, value)
model.metadata ||= Metadata.new
model.metadata.category = value["elements"]["category"].text
model.metadata.identifier = value["elements"]["identifier"].text
end
end
<CustomCeramic Size="15">
<Name>XML Masterpiece: Vase</Name>
XML Description: A beautiful ceramic vase
<metadata>
<category>Metadata</category>
<identifier>123</identifier>
</metadata>
</CustomCeramic>
> CustomCeramic.from_xml(xml)
> #<CustomCeramic:0x0000000108d0e1f8
@element_order=["text", "Name", "text", "Size", "text"],
@name="Masterpiece: Vase",
@ordered=nil,
@size=12,
@description="A beautiful ceramic vase",
@metadata=#<Metadata:0x0000000105ad52e0 @category="Metadata", @identifier="123">>
> puts CustomCeramic.new(name: "Vase", size: 12, description: "A beautiful vase", metadata: Metadata.new(category: "Glaze", identifier: 15)).to_xml
# <CustomCeramic Size="15">
# <Name>XML Masterpiece: Vase</Name>
# <metadata>
# <category>Glaze</category>
# <identifier>15</identifier>
# </metadata>
# XML Description: A beautiful vase
# </CustomCeramic>
def custom_method_from_xml(model, value)
instance = value.node # Lutaml::Model::XmlAdapter::AdapterElement
# OR
instance = value.node.adapter_node # Adapter::Element
xml = instance.to_xml
end
When building a model from XML in custom methods, if the value
parameter is a mapping_hash
, then it allows access to the parsed XML structure through value.node
which can be converted to an XML string using to_xml
.
Note
|
For NokogiriAdapter , we can also call to_xml on value.node.adapter_node .
|
> value
> # {"text"=>["\n ", "\n ", "\n "], "elements"=>{"category"=>{"text"=>"Metadata"}}}
> value.to_xml
> # undefined_method `to_xml`
> value.node
# Nokogiri Adapter Node
#<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656ed8
# @attributes={},
# @children=
# [#<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656cd0 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">,
# #<Lutaml::Model::XmlAdapter::NokogiriElement:0x00000001076569b0
# @attributes={},
# @children=
# [#<Lutaml::Model::XmlAdapter::NokogiriElement:0x00000001076567f8 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
# @default_namespace=nil,
# @name="category",
# @namespace_prefix=nil,
# @text="Metadata">,
# #<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656028 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">],
# @default_namespace=nil,
# @name="metadata",
# @namespace_prefix=nil,
# @text="\n Metadata\n ">
# Ox Adapter Node
#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584f78
# @attributes={},
# @children=
# [#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584e60
# @attributes={},
# @children=[#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584d48 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
# @default_namespace=nil,
# @name="category",
# @namespace_prefix=nil,
# @text="Metadata">],
# @default_namespace=nil,
# @name="metadata",
# @namespace_prefix=nil,
# @text=nil>
# Oga Adapter Node
# <Lutaml::Model::XmlAdapter::Oga::Element:0x0000000107314158
# @attributes={},
# @children=
# [#<Lutaml::Model::XmlAdapter::Oga::Element:0x0000000107314090 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">,
# #<Lutaml::Model::XmlAdapter::Oga::Element:0x000000010730fe78
# @attributes={},
# @children=[#<Lutaml::Model::XmlAdapter::Oga::Element:0x000000010730fd88 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
# @default_namespace=nil,
# @name="category",
# @namespace_prefix=nil,
# @text="Metadata">,
# #<Lutaml::Model::XmlAdapter::Oga::Element:0x000000010730f8d8 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">],
# @default_namespace=nil,
# @name="metadata",
# @namespace_prefix=nil,
# @text="\n Metadata\n ">
> value.node.to_xml
> #<metadata><category>Metadata</category></metadata>
hsh | json | yaml | toml do
map 'attribute_name', to: :name_of_attribute, with: {
to: :method_name_to_serialize,
from: :method_name_to_deserialize
}
end
with:
key to define custom serialization methodsThe following class will parse the JSON snippet below:
class CustomCeramic < Lutaml::Model::Serializable
attribute :name, :string
attribute :size, :integer
json do
map 'name', to: :name, with: { to: :name_to_json, from: :name_from_json }
map 'size', to: :size
end
def name_to_json(model, doc)
doc["name"] = "Masterpiece: #{model.name}"
end
def name_from_json(model, value)
model.name = value.sub(/^Masterpiece: /, '')
end
end
{
"name": "Masterpiece: Vase",
"size": 12
}
> CustomCeramic.from_json(json)
> #<CustomCeramic:0x0000000104ac7240 @name="Vase", @size=12>
> CustomCeramic.new(name: "Vase", size: 12).to_json
> #{"name"=>"Masterpiece: Vase", "size"=>12}
Different information models define different primitive value types, and the same goes for the notions of the "missing values" family:
- the empty value
-
the value is present but empty
- the non-existent value
-
the value is not present
- the undefined value
-
the value is not defined
There are also different ways to represent these missing values when the attribute accepts a single value or a collection of values.
Technology | Missing value type | Realized as |
---|---|---|
Lutaml::Model |
empty value |
Ruby empty string ( |
non-existent value |
Ruby |
|
undefined value |
|
|
XML element |
empty value |
XML blank element: |
non-existent value |
XML blank element with attribute |
|
undefined value |
the XML element is omitted |
|
XML attribute |
empty value |
XML blank attribute: |
non-existent value |
the XML attribute is omitted |
|
undefined value |
the XML attribute is omitted |
|
JSON |
empty value |
JSON empty string ( |
non-existent value |
JSON |
|
undefined value |
the JSON key is omitted |
|
TOML |
empty value |
TOML empty string |
non-existent value |
the TOML key is omitted since TOML does not support the concept of null. |
|
undefined value |
the TOML key is omitted |
Note
|
The Uninitialized class is a special Lutaml::Model construct, it is not
supported by normal Ruby objects.
|
The challenge for the developer is how to represent fully compatible semantics using interoperable data models across different technologies.
Lutaml::Model provides you with several mechanisms to retain the missing values semantics. An example mapping is shown in the following diagram.
┌─────────────────────────────────────────────────────────────────────────────┐
│ ╔════════════════════════╗ ╔════════════════════════╗ │
│ ║ Lutaml::Model Values ║ ║ YAML Values ║ │
│ ╠════════════════════════╣ ╠════════════════════════╣ │
│ ║ ║ mapping ║ ║ │
│ ║ "empty" ║◀──────────────────▶║ "empty" ║ │
│ ║ (empty string, []) ║ to empty ║ (empty string, []) ║ │
│ ║ ║ ║ ║ │
│ ╟────────────────────────╢ ╟────────────────────────╢ │
│ ║ ║ mapping ║ ║ │
│ ║ "non-existent" ║◀──────────────────▶║ "non-existent" ║ │
│ ║ (nil) ║ to non-existent ║ (null) ║ │
│ ║ ║ ║ ║ │
│ ╟────────────────────────╢ ╟────────────────────────╢ │
│ ║ ║ mapping ║ ║ │
│ ║ "undefined" ║◀──────────────────▶║ "undefined" ║ │
│ ║ (uninitialized) ║ to undefined ║ (key omitted) ║ │
│ ║ ║ ║ ║ │
│ ╚════════════════════════╝ ╚════════════════════════╝ │
└─────────────────────────────────────────────────────────────────────────────┘
In the case where the interoperating technologies do not support the full spectrum of missing value types, it is necessary for the developer to understand any such behavior and relevant handling.
┌─────────────────────────────────────────────────────────────────────────────┐
│ ╔════════════════════════╗ ╔════════════════════════╗ │
│ ║ Lutaml::Model Values ║ ║ TOML Values ║ │
│ ╠════════════════════════╣ ╠════════════════════════╣ │
│ ║ ║ mapping ║ ║ │
│ ║ "empty" ║◀──────────────────▶║ "empty" ║ │
│ ║ (empty string, []) ║ to empty ║ (empty string, []) ║ │
│ ║ ║ ║ ║ │
│ ╟────────────────────────╢ ╟────────────────────────╢ │
│ ║ ║ mapping ║ ║ │
│ ║ "non-existent" ║◀──────────────────▶║ ║ │
│ ║ (nil) ║ to undefined ║ "undefined" ║ │
│ ║ ║ ║ (key omitted) ║ │
│ ╟────────────────────────╢ ║ ║ │
│ ║ ║ mapping ║ ║ │
│ ║ "undefined" ║─────(one-way)─────▶║ TOML does not ║ │
│ ║ (uninitialized) ║ to undefined ║ support NULL ║ │
│ ║ ║ ║ ║ │
│ ╚════════════════════════╝ ╚════════════════════════╝ │
└─────────────────────────────────────────────────────────────────────────────┘
There are the following additional challenges that a developer must take into account of:
-
Single attribute value vs collection attribute value. Different technologies treat single/collection values differently.
-
External schemas and systems that interoperate with serializations from Lutaml::Model. Many schemas and systems adopt "different" conventions for representing missing value semantics (sometimes very awkward ones).
The solution for the first challenge is to understand the behavior of the different technologies used. The default mappings are described in Value representation in Lutaml::Model and Value representation in serialization formats.
The following table summarizes the behavior of the Lutaml::Model in regards of the "missing values" family.
LutaML value type | Cardinality (1 or n) | Missing value type | Ruby value |
---|---|---|---|
Collection attribute |
collection |
empty value |
|
non-existent value |
|
||
undefined value |
No assigned value |
||
|
single |
empty value |
|
non-existent value |
|
||
undefined value |
No assigned value |
||
|
single |
empty value |
N/A |
non-existent value |
|
||
undefined value |
No assigned value |
||
|
single |
empty value |
N/A |
non-existent value |
|
||
undefined value |
No assigned value |
||
|
single |
empty value |
N/A |
non-existent value |
|
||
undefined value |
No assigned value |
||
|
single |
empty value |
N/A |
non-existent value |
|
||
undefined value |
No assigned value |
||
|
single |
empty value |
N/A |
non-existent value |
|
||
undefined value |
No assigned value |
||
|
single |
empty value |
N/A |
non-existent value |
|
||
undefined value |
No assigned value |
||
|
single |
empty value |
N/A |
non-existent value |
|
||
undefined value |
No assigned value |
||
|
single |
empty value |
N/A |
non-existent value |
|
||
undefined value |
No assigned value |
||
|
single |
empty value |
|
non-existent value |
|
||
undefined value |
No assigned value |
Every serialization format uses a different information model to represent these missing values.
Some serialization formats support all 3 types of missing values, while others only support a subset of them.
Serialization format | Cardinality (1 or n) | Missing value type | Example |
---|---|---|---|
XML |
collection |
empty collection |
the XML blank element: |
non-existent collection |
a blank element with attribute |
||
undefined collection |
the XML element is omitted |
||
single |
empty value |
the XML blank element: |
|
non-existent value |
a blank element with attribute |
||
undefined value |
the XML element is omitted |
||
JSON |
collection |
empty collection |
an empty array ( |
non-existent collection |
the value |
||
undefined collection |
the key is omitted |
||
single |
empty value |
an empty string ( |
|
non-existent value |
the value |
||
undefined value |
the key is omitted |
||
YAML |
collection |
empty collection |
an empty array ( |
non-existent collection |
the value |
||
undefined collection |
the key is omitted |
||
single |
empty value |
an empty string ( |
|
non-existent value |
the value |
||
undefined value |
the key is omitted |
||
TOML |
collection |
empty collection |
an empty array ( |
non-existent collection |
TOML does not support the concept of "null" |
||
undefined collection |
the key is omitted |
||
single |
empty value |
an empty string ( |
|
non-existent value |
TOML does not support the concept of "null" |
||
undefined value |
the key is omitted |
Lutaml::Model provides a comprehensive way to handle the missing values family across different serialization formats.
The value_map
option as applied to serialization mapping rules allow users to
meticulously define how each and every missing value should be mapped from a
serialization format to a Lutaml::Model object.
The value_map
option is used to define mappings for both from
and to
values:
from
pairs-
A hash of key-value pairs that determines the mapping of a missing value at the serialization format ("from") to a LutaML Model missing value where this mapping applies. The key is the missing value type in the serialization format, and the value is the missing value type in the LutaML Model.
NoteIn other words, used when converting the serialized format into a Lutaml::Model Ruby object. to
pairs-
A hash of key-value pairs that determines the mapping of a LutaML Model ("to") missing to a missing value choice at the serialization format where this mapping applies. The key is the missing value type in the LutaML Model, and the value is the missing value type in the serialization format.
NoteIn other words, used when converting a Lutaml::Model Ruby object into the serialized format.
Syntax:
{map_command} 'format-key', to: :attribute_name, value_map: { (1)
from: {
{format-missing-value-n}: {model-missing-value-n}, (2)
{format-missing-value-m}: {model-missing-value-m},
{format-missing-value-o}: {model-missing-value-o}
},
to: {
{model-missing-value-n}: {format-missing-value-n}, (3)
{model-missing-value-m}: {format-missing-value-m},
{model-missing-value-o}: {format-missing-value-o}
}
}
-
The
{map_command}
is a mapping rule with the actual command depending on the serialization format. -
In the
from
mapping, the keys are the missing value types in the serialization format. -
In the
to
mapping, the keys are the missing value types in the LutaML Model.
The missing value type mapping differs per serialization format, as serialization formats may not fully support all missing value types.
The availability of from
and to
keys and values depend on the types of
missing values supported by that particular serialization format.
The available values for from
and to
for serialization formats
are presented below, where the allowed values are to be used in the direction
of the format. That means if the format supports :empty
, it can be used
as a key in from:
direction, and the value in the to:
direction (see {format-missing-value-n}
) in the syntax.
Map command | Missing value types available (key in from: direction, value in the to: direction) |
---|---|
XML element |
|
XML attribute |
|
Hash |
|
JSON |
|
YAML |
|
TOML |
|
nil
cannot be used; therefore in a from:
value map, it is not possible to indicate nil: {model-missing-value}
.
In an XML mapping block, it is possible to do the following.
xml do
map_element 'status', to: :status, value_map: {
from: { empty: :nil, omitted: :omitted, nil: :nil },
to: { empty: :nil, omitted: :omitted, nil: :nil }
}
end
Each serialization format has specific behavior when handling values
such as empty
, omitted
, and nil
.
Users can specify the mapping for both from
and to
values using the
value_map
option in the attribute definition.
-
The keys that can be used in the
from
andto
mappings areempty
,omitted
, andnil
. -
The values in the mappings can also be
empty
,omitted
, andnil
.
Note
|
Since nil is not supported in TOML, so mappings like nil:
{any_option} or {any_option}: :nil will not work in TOML.
|
Note
|
In a collection attribute, the values of value_map also depend on the
initialize_empty setting, where an omitted value in the serialization format can still lead to a nil
or an empty array [] at the attribute-level (instead of the mapping-level).
|
The table below describes the default value_map
configurations for supported
serialization formats.
attribute :attr, :string
xml do
map_element 'key', to: :attr, value_map: {
from: { empty: :nil, omitted: :omitted, nil: :nil },
to: { empty: :empty, omitted: :omitted, nil: :nil }
}
end
Direction |
Map rule |
XML source |
Model target |
---|---|---|---|
|
|
blank XML element ( |
|
|
absent XML element |
omitted from the model |
|
|
blank XML element with attribute |
|
|
Direction |
Map rule |
Model source |
XML target |
|
|
empty string ( |
blank XML element |
|
omitted in the model |
XML element not rendered |
|
|
|
blank XML element with attribute |
attribute :attr, :string, collection: true
xml do
map_element 'key', to: :attr, value_map: {
from: { empty: :empty, omitted: :omitted, nil: :nil },
to: { empty: :empty, omitted: :omitted, nil: :nil }
}
end
Direction |
Map rule |
XML source |
Model target |
---|---|---|---|
|
|
blank XML element ( |
empty array ( |
|
absent XML element |
omitted from the model |
|
|
blank XML element with attribute |
|
|
Direction |
Map rule |
Model source |
XML target |
|
|
empty array ( |
blank XML element |
|
omitted in the model |
XML element not rendered |
|
|
|
blank XML element with attribute |
attribute :attr, :string
xml do
map_attribute 'attr_name', to: :attr, value_map: {
from: { empty: :nil, omitted: :omitted },
to: { empty: :empty, nil: :empty, omitted: :omitted }
}
end
Direction |
Map rule |
XML source |
Model target |
---|---|---|---|
|
|
blank XML attribute ( |
|
|
absent XML attribute |
omitted from the model |
|
Direction |
Map rule |
Model source |
XML target |
|
|
empty string ( |
blank XML attribute ( |
|
omitted in the model |
XML attribute not rendered |
|
|
|
blank XML attribute ( |
attribute :attr, :string, collection: true
xml do
map_attribute 'attr_name', to: :attr, value_map: {
from: { empty: :empty, omitted: :omitted },
to: { empty: :empty, nil: :omitted, omitted: :omitted }
}
end
Direction |
Map rule |
XML source |
Model target |
---|---|---|---|
|
|
blank XML attribute ( |
empty array ( |
|
absent XML attribute |
omitted from the model |
|
Direction |
Map rule |
Model source |
XML target |
|
|
empty array ( |
blank XML attribute ( |
|
omitted in the model |
XML attribute not rendered |
|
|
|
XML attribute not rendered |
attribute :attr, :string
yaml do
map 'key', to: :attr, value_map: {
from: { empty: :empty, omitted: :omitted, nil: :nil },
to: { empty: :empty, omitted: :omitted, nil: :nil }
}
end
Direction |
Map rule |
XML source |
Model target |
---|---|---|---|
|
|
empty string in YAML ( |
empty string ( |
|
absent YAML key |
omitted from the model |
|
|
YAML |
|
|
Direction |
Map rule |
Model source |
XML target |
|
|
empty string ( |
empty string in YAML ( |
|
omitted in the model |
YAML key omitted |
|
|
|
YAML |
Note
|
In order to treat a YAML value like status: '' to nil , the mapping of
value_map: { from: { empty: :nil } } can be applied.
|
attribute :attr, :string, collection: true
yaml do
map 'key', to: :attr, value_map: {
from: { empty: :empty, omitted: :omitted, nil: :nil },
to: { empty: :empty, omitted: :omitted, nil: :nil }
}
end
Direction |
Map rule |
YAML source |
Model target |
---|---|---|---|
|
|
empty YAML array ( |
empty array ( |
|
absent YAML key |
omitted from the model |
|
|
YAML |
|
|
Direction |
Map rule |
Model source |
YAML target |
|
|
empty array ( |
empty YAML array ( |
|
omitted in the model |
YAML key omitted |
|
|
|
YAML |
Note
|
If the YAML key for the collection attribute is omitted, it will be treated
as nil or an empty array depending on the initialize_empty setting.
|
attribute :attr, :string
json do
map 'key', to: :attr, value_map: {
from: { empty: :empty, omitted: :omitted, nil: :nil },
to: { empty: :empty, omitted: :omitted, nil: :nil }
}
end
Direction |
Map rule |
JSON source |
Model target |
---|---|---|---|
|
|
empty string in JSON ( |
empty string ( |
|
absent JSON key |
omitted from the model |
|
|
JSON |
|
|
Direction |
Map rule |
Model source |
JSON target |
|
|
empty string ( |
empty string in JSON ( |
|
omitted in the model |
JSON key omitted |
|
|
|
JSON |
attribute :attr, :string, collection: true
json do
map 'key', to: :attr, value_map: {
from: { empty: :empty, omitted: :omitted, nil: :nil },
to: { empty: :empty, omitted: :omitted, nil: :nil }
}
end
Direction |
Map rule |
JSON source |
Model target |
---|---|---|---|
|
|
empty JSON array ( |
empty array ( |
|
absent JSON key |
omitted from the model |
|
|
JSON |
|
|
Direction |
Map rule |
Model source |
JSON target |
|
|
empty array ( |
empty JSON array ( |
|
omitted in the model |
JSON key omitted |
|
|
|
JSON |
TOML does not support the concept of nil
and therefore the mapping of from:
direction with nil
to will not work in TOML.
The nil
mapping is only supported in the to:
direction (model to TOML).
attribute :attr, :string
toml do
map 'key', to: :attr, value_map: {
from: { empty: :empty, omitted: :omitted },
to: { empty: :empty, omitted: :omitted, nil: :omitted }
}
end
Direction |
Map rule |
TOML source |
Model target |
---|---|---|---|
|
|
empty string in TOML ( |
empty string ( |
|
absent TOML key |
omitted from the model |
|
Direction |
Map rule |
Model source |
TOML target |
|
|
empty string ( |
empty string in TOML ( |
|
omitted in the model |
TOML key omitted |
|
|
|
TOML key omitted |
TOML does not support the concept of nil
and therefore the mapping of from:
direction with nil
to will not work in TOML.
The nil
mapping is only supported in the to:
direction (model to TOML).
attribute :attr, :string, collection: true
toml do
map 'key', to: :attr, value_map: {
from: { empty: :empty, omitted: :omitted },
to: { empty: :empty, omitted: :omitted, nil: :omitted }
}
end
Direction |
Map rule |
TOML source |
Model target |
---|---|---|---|
|
|
empty TOML array ( |
empty array ( |
|
absent TOML key |
omitted from the model |
|
Direction |
Map rule |
Model source |
TOML target |
|
|
empty array ( |
empty TOML array ( |
|
omitted in the model |
TOML key omitted |
|
|
|
TOML key omitted |
The value_map
option can be defined to meticulously map for each serialization
format as follows.
value_map
with from
and to
valuesclass ExampleClass < Lutaml::Model::Serializable
attribute :status, :string
xml do
map_element 'status', to: :status, value_map: {
from: { empty: :nil, omitted: :omitted, nil: :nil },
to: { empty: :nil, omitted: :omitted, nil: :nil }
}
end
hsh | json | yaml | toml | key_value do
map 'status', to: :status, value_map: {
from: { empty: :nil, omitted: :omitted, nil: :nil },
to: { empty: :nil, omitted: :omitted, nil: :nil }
}
end
end
yaml = <<~YAML
---
status: ''
YAML
ExampleClass.from_yaml(yaml)
# => #<ExampleClass:0x000000011954efb0 @status=nil>
yaml1 = <<~YAML
---
YAML
ExampleClass.from_yaml(yaml1)
# => #<ExampleClass:0x000000011954efb0 @status=uninitialized>
yaml2 = <<~YAML
---
status:
YAML
ExampleClass.from_yaml(yaml2)
# => #<ExampleClass:0x000000011954efb0 @status=nil>
When defining an attribute with collection: true
, the attribute will behave as follows:
attribute :status, :string, collection: true
Here’s an example of how you can use the value_map
with a collection attribute.
value_map
with a collection attributeclass ExampleClass < Lutaml::Model::Serializable
attribute :status, :string, collection: true
xml do
map_element 'status', to: :status, value_map: {
from: { empty: :nil, omitted: :omitted, nil: :nil },
to: { empty: :nil, omitted: :omitted, nil: :nil }
}
end
hsh | json | yaml | key_value do
map 'status', to: :status, value_map: {
from: { empty: :nil, omitted: :omitted, nil: :nil },
to: { empty: :nil, omitted: :omitted, nil: :nil }
}
end
toml do
map 'status', to: :status, value_map: {
from: { empty: :nil, omitted: :omitted },
to: { empty: :nil, omitted: :omitted, nil: :omitted }
}
end
end
yaml = <<~YAML
---
status: ['new', 'assigned']
YAML
y = ExampleClass.from_yaml(yaml)
# => #<ExampleClass:0x000000011954efb0 @status=["new", "assigned"]>
There are times that one may want to simply override handling of selective missing value types rather than re-define the entire value map.
The :render_*
and :treat_*
options are simple switches that override the default
value map provided for the different serialization formats.
Syntax:
{map_command} 'format-key', to: :attribute_name, (1)
:render_{model-value}: :as_{format-value}, (2)
# ...
:treat_{format-value}: :as_{model-value}, (3)
# ...
-
The
{map_command}
is a mapping rule with the actual command depending on the serialization format. The attribute ofattribute_name
may be a single or a collection value. -
The
:render_*
mapping overrides the default value map for missing value types in model-to-serialization. -
The
:treat_*
mapping overrides the default value map for missing value types in serialization-to-model.
Specifically,
-
The
:render_{model-value}: :as_{format-value}
options are used to override the default behavior of rendering missing value types into the serialization format.{model-value}
-
specifies the missing value type in the LutaML Model.
{format-value}
-
specifies the missing value type in the serialization format.
-
The
:treat_{format-value}: :as_{model-value}
options are used to override the default behavior of importing missing value types into the model.{format-value}
-
specifies the missing value type in the serialization format.
{model-value}
-
specifies the missing value type in the LutaML Model.
In effect, the default value_map
is overriden by the :render_*
and :treat_*
directives.
Given the default mapping for an XML element, the :render_*
and :treat_*
options can be used to selectively override behavior.
xml do
map_element 'key', to: :attr, value_map: {
from: { empty: :nil, omitted: :omitted, nil: :nil },
to: { empty: :empty, omitted: :omitted, nil: :nil }
}
end
By changing to this:
xml do
map_element 'key', to: :attr,
render_nil: :as_empty, (1)
treat_omitted: :as_nil (2)
end
-
This overrides the
to:
directionnil: :nil
mapping. -
This overrides the
from:
directionomitted: :omitted
mapping
The resulting value map would be:
xml do
map_element 'status', to: :status, value_map: {
from: { empty: :nil, omitted: :omitted, nil: :empty }, (1)
to: { empty: :nil, omitted: :nil, nil: :nil } (2)
}
end
-
See that
nil: :nil
is nownil: :empty
. -
See that
omitted: :omitted
is nowomitted: :nil
:render_nil
is a specially handled case of the :render_*
pattern due to
legacy. It is used to override default value map behavior for the nil
model
value.
render_nil
accepts these values:
:as_empty
/:as_blank
-
if the value is nil, render it as an empty string.
:nil
-
if the value is nil, render it as an element with attribute
xsi:nil
. :omit
-
if the value is nil, omit the element or attribute.
true
-
(legacy) setting
render_nil: true
will render the attribute as an empty element if the attribute isnil
. This has the same effect asrender_nil: :as_empty
.
Syntax:
xml do
map_element 'key_value_model_attribute_name', to: :name_of_attribute, render_nil: {option}
end
hsh | json | yaml | toml do
map 'key_value_model_attribute_name', to: :name_of_attribute, render_nil: {option}
end
render_nil: true
option to render an attribute value of nil
as an empty elementclass Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, :string
xml do
map_element 'type', to: :type, render_nil: true
map_element 'glaze', to: :glaze
end
json do
map 'type', to: :type, render_nil: true
map 'glaze', to: :glaze
end
end
> Ceramic.new.to_json
> # { 'type': null }
> Ceramic.new(type: "Porcelain", glaze: "Clear").to_json
> # { 'type': 'Porcelain', 'glaze': 'Clear' }
> Ceramic.new.to_xml
> # <Ceramic><type></type></Ceramic>
> Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
> # <Ceramic><type>Porcelain</type><glaze>Clear</glaze></Ceramic>
render_nil: true
option to render an empty attribute collection of nil
as an empty elementclass Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glazes, :string, collection: true
xml do
map_element 'type', to: :type, render_nil: true
map_element 'glazes', to: :glazes, render_nil: true
end
json do
map 'type', to: :type, render_nil: true
map 'glazes', to: :glazes, render_nil: true
end
end
> Ceramic.new.to_json
> # { 'type': null, 'glazes': [] }
> Ceramic.new(type: "Porcelain", glazes: ["Clear"]).to_json
> # { 'type': 'Porcelain', 'glazes': ['Clear'] }
> Ceramic.new.to_xml
> # <Ceramic><type></type><glazes></glazes></Ceramic>
> Ceramic.new(type: "Porcelain", glazes: ["Clear"]).to_xml
> # <Ceramic><type>Porcelain</type><glazes>Clear</glazes></Ceramic>
Using render_nil: :omit
with a nil
value will omit the key from XML and
key-value formats.
class SomeModel < Lutaml::Model::Serializable
attribute :coll, :string, collection: true
xml do
root "some-model"
map_element 'collection', to: :coll, render_nil: :omit
end
key_value do
map 'collection', to: :coll, render_nil: :omit
end
end
puts SomeModel.new.coll
# => nil
puts SomeModel.new.to_xml
# =>
# <some-model></some-model>
puts SomeModel.new.to_yaml
# =>
# ---
Using render_nil: :as_nil
with a nil
value will create an empty element with
xsi:nil
attribute in XML and create a key with explicit null value in
key-value formats.
Note
|
TOML does not support this option.
|
class SomeModel < Lutaml::Model::Serializable
attribute :coll, :string, collection: true
xml do
root "some-model"
map_element 'collection', to: :coll, render_nil: :as_nil
end
hsh | json | yaml do
map 'collection', to: :coll, render_nil: :as_nil
end
end
puts SomeModel.new.coll
# => nil
puts SomeModel.new.to_xml
# =>
# <some-model xsi:xmlns="..."><collection xsi:nil="true"/></some-model>
puts SomeModel.new.to_yaml
# =>
# ---
# coll: null
Using render_nil: :as_blank | :as_empty
will create a blank element in XML and
create a key with an explicit empty array in key-value formats.
class SomeModel < Lutaml::Model::Serializable
attribute :coll, :string, collection: true
xml do
root "some-model"
map_element 'collection', to: :coll, render_nil: :as_blank
end
key_value do
map 'collection', to: :coll, render_nil: :as_empty
end
end
puts SomeModel.new.coll
# => nil
puts SomeModel.new.to_xml
# =>
# <some-model><collection/></some-model>
puts SomeModel.new.to_yaml
# =>
# ---
# coll: []
:render_empty
is a specially handled case of the :render_*
pattern due to
legacy. It is used to override default value map behavior for the nil
model
value.
render_empty
accepts these values:
:as_empty
/:as_blank
-
if the value is nil, render it as an empty string.
:nil
-
if the value is nil, render it as an element with attribute
xsi:nil
. :omit
-
if the value is nil, omit the element or attribute.
Syntax:
xml do
map_element 'key_value_model_attribute_name', to: :name_of_attribute, render_empty: {option}
end
hsh | json | yaml | toml do
map 'key_value_model_attribute_name', to: :name_of_attribute, render_empty: {option}
end
Using render_empty: :omit
with an empty value or empty collection will omit
the key from XML and key-value formats.
class SomeModel < Lutaml::Model::Serializable
attribute :coll, :string, collection: true
xml do
root "some-model"
map_element 'collection', to: :coll, render_empty: :omit
end
key_value do
map 'collection', to: :coll, render_empty: :omit
end
end
puts SomeModel.new(coll: []).coll
# => []
puts SomeModel.new.to_xml
# =>
# <some-model></some-model>
puts SomeModel.new.to_yaml
# =>
# ---
Using render_empty: :as_nil
will create an empty element with the xsi:nil
attribute in XML, and create a key with explicit null value in key-value formats.
Note
|
TOML does not support this option.
|
class SomeModel < Lutaml::Model::Serializable
attribute :coll, :string, collection: true
xml do
root "some-model"
map_element 'collection', to: :coll, render_empty: :as_nil
end
hsh | json | yaml do
map 'collection', to: :coll, render_empty: :as_nil
end
end
puts SomeModel.new(coll: []).coll
# => []
puts SomeModel.new.to_xml
# =>
# <some-model xsi:xmlns="..."><collection xsi:nil="true"/></some-model>
puts SomeModel.new.to_yaml
# =>
# ---
# coll: null
Using render_empty: :as_blank
or render_empty: :as_empty
will create a blank
element in XML and create a key with an explicit empty array in key-value
formats.
class SomeModel < Lutaml::Model::Serializable
attribute :coll, :string, collection: true
xml do
root "some-model"
map_element 'collection', to: :coll, render_empty: :as_blank
end
key_value do
map 'collection', to: :coll, render_empty: :as_empty
end
end
puts SomeModel.new(coll: []).coll
# => []
puts SomeModel.new.to_xml
# =>
# <some-model><collection/></some-model>
puts SomeModel.new.to_yaml
# =>
# ---
# coll: []
Lutaml::Model provides a way to import data models defined from various formats into the LutaML data modeling system.
Data model languages supported are:
The following figure illustrates the process of importing an XML Schema model to create LutaML core models. Once the LutaML core models are created, they can be used to parse and generate XML documents according to the imported XML Schema model.
Today, the LutaML core models are written into Ruby files, which can be used to parse and generate XML documents according to the imported XML Schema. This is to be changed so that the LutaML core models are directly loaded and interpreted.
╔════════════════════════════╗ ╔═══════════════════════╗
║ Serialization Models ║ ║ Core Model ║
╚════════════════════════════╝ ╚═══════════════════════╝
╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆ XML Schema (XSD/RNG/RNC) ┆ ┆ Model ┆
┆ │ ┆ ┌────────────────┐ ┆ │ ┆
┆ ┌──────┴──────┐ ┆ │ │ ┆ ┌────────┴──┐ ┆
┆ │ │ ┆ │ Model │ ┆ │ │ ┆
┆ Models Value Types ┆──►│ Importing │──►┆ Models Value Types ┆
┆ │ │ ┆ │ │ ┆ │ │ ┆
┆ │ │ ┆ └────────────────┘ ┆ │ │ ┆
┆ ┌────┴────┐ ┌─┴─┐ ┆ │ ┆ │ ┌──────┴──┐ ┆
┆ │ │ │ │ ┆ │ ┆ │ │ │ ┆
┆ Element Value xs:string ┆ │ ┆ │ String Integer ┆
┆ Attribute Type xs:date ┆ │ ┆ │ Date Float ┆
┆ Union Complex xs:boolean ┆ │ ┆ │ Time Boolean ┆
┆ Sequence Choice xs:anyURI ┆ │ ┆ │ ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯ │ ┆ └──────┐ ┆
│ ┆ │ ┆
│ ┆ Contains ┆
│ ┆ more Models ┆
│ ┆ (recursive) ┆
│ ┆ ┆
│ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
│ ┌────────────────┐
│ │ │
│ │ Model │
└──────────► │ Transformation │
│ & │
│ Mapping Rules │
│ │
└────────────────┘
W3C XSD is a schema language designed to define the structure of XML documents, alongside other XML schema languages like DTD, RELAX NG, and Schematron.
Lutaml::Model supports the import of XSD schema files to define information models that can be used to parse and generate XML documents.
Specifically, the Lutaml::Model::Schema#from_xml
method loads XML Schema files
(XSD, .xsd
) and generates Ruby files (.rb
) that inherit from
Lutaml::Model::Serializable
that are saved to disk.
Syntax:
Lutaml::Model::Schema.from_xml(
xsd_schema, (1)
options: options (2)
)
-
The
xsd_schema
is the XML Schema string to be converted to model files. -
The
options
hash is an optional argument.options
-
Optional hash containing potentially the following key-values.
output_dir
-
The directory where the model files will be saved. If not provided, a default directory named
lutaml_models_<timestamp>
is created."path/to/directory"
create_files
-
A
boolean
argument (false
by default) to create files directly in the specified directory as defined by theoutput_dir
option.create_files: (true | false)
load_classes
-
A
boolean
argument (false
by default) to load generated classes before returning them.load_classes: (true | false)
namespace
-
The namespace of the schema. This will be added in the
Lutaml::Model::Serializable
file’sxml do
block. prefix
-
The prefix of the namespace provided in the
namespace
option.example-prefix
location
-
The URL or path of the directory containing all the files of the schema. For more information, refer to the XML Schema specification.
"http://example.com/example.xsd"
"path/to/schema/directory"
Note
|
If both create_files and load_classes are provided, the create_files argument will take priority and generate files without loading them!
|
The generated LutaML models consists of two different kind of Ruby classes depending on the XSD schema:
- XSD "SimpleTypes"
-
converted into classes that inherit from
Lutaml::Model::Type::Value
, which define the data types with restrictions and other validations of these values. - XSD "ComplexTypes"
-
converted into classes that inherit from
Lutaml::Model::Serializable
that model according to the defined structure.
Lutaml::Model uses the lutaml-xsd
gem to
automatically resolve the include
and import
elements, enabling
Lutaml-Model to generate the corresponding model files.
This auto-resolving feature allows seamless integration of these files into your models without the need for manual resolution of includes and imports.
Lutaml::Model::Schema#from_xml
to convert an XML Schema to model filesxsd_schema = <<~XSD
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
/* your schema here */
</xs:schema>
XSD
options = {
# These are all optional:
output_dir: 'path/to/directory',
namespace: 'http://example.com/namespace',
prefix: "example-prefix",
location: "http://example.com/example.xsd",
# or
# location: "path/to/schema/directory"
create_files: true, # Default: false
# OR
load_classes: true, # Default: false
}
# generates the files in the output_dir | default_dir
Lutaml::Model::Schema.from_xml(xsd_schema, options: options)
You could also directly load the generated Ruby files into your application by requiring them.
Lutaml::Model::Schema.from_xml(xsd_schema, options: {output_dir: 'path/to/directory'})
require_relative 'path/to/directory/*.rb'
Lutaml::Model provides a way to validate data models using the validate
and
validate!
methods.
-
The
validate
method sets anerrors
array in the model instance that contains all the validation errors. This method is used for checking the validity of the model silently. -
The
validate!
method raises aLutaml::Model::ValidationError
that contains all the validation errors. This method is used for forceful validation of the model through raising an error.
Lutaml::Model supports the following validation methods:
-
collection
:: Validates collection size range. -
values
:: Validates the value of an attribute from a set of fixed values. -
choice
:: Validates that attribute specified within defined range
The following class will validate the degree_settings
attribute to ensure that
it has at least one element and that the description
attribute is one of the
values in the set [one, two, three]
.
class Kiln < Lutaml::Model::Serializable
attribute :name, :string
attribute :degree_settings, :integer, collection: (1..)
attribute :description, :string, values: %w[one two three]
attribute :id, :integer
attribute :age, :integer
choice(min: 1, max: 1) do
choice(min: 1, max: 2) do
attribute :prefix, :string
attribute :forename, :string
end
attribute :nick_name, :string
end
xml do
map_element 'name', to: :name
map_attribute 'degree_settings', to: :degree_settings
end
end
kiln = Kiln.new(name: "Kiln", degree_settings: [100, 200, 300], description: "one", prefix: "Ben")
kiln.validate
# => []
kiln = Kiln.new(name: "Kiln", degree_settings: [], description: "four", prefix: "Ben", nick_name: "Smith")
kiln.validate
# => [
# #<Lutaml::Model::CollectionSizeError: degree_settings must have at least 1 element>,
# #<Lutaml::Model::ValueError: description must be one of [one, two, three]>,
# #<Lutaml::Model::ChoiceUpperBoundError: Attribute count exceeds the upper bound>
# ]
e = kiln.validate!
# => Lutaml::Model::ValidationError: [
# degree_settings must have at least 1 element,
# description must be one of [one, two, three],
# Attribute count exceeds the upper bound
# ]
e.errors
# => [
# #<Lutaml::Model::CollectionSizeError: degree_settings must have at least 1 element>,
# #<Lutaml::Model::ValueError: description must be one of [one, two, three]>,
# #<Lutaml::Model::ChoiceUpperBoundError: Attribute count exceeds the upper bound>
# #<Lutaml::Model::ChoiceLowerBoundError: Attribute count is less than lower bound>
# ]
To add custom validation, override the validate
method in the model class.
Additional errors should be added to the errors
array.
The following class validates the degree_settings
attribute when the type
is
glass
to ensure that the value is less than 1300.
class Kiln < Lutaml::Model::Serializable
attribute :name, :string
attribute :type, :string, values: %w[glass ceramic]
attribute :degree_settings, :integer, collection: (1..)
def validate
errors = super
if type == "glass" && degree_settings.any? { |d| d > 1300 }
errors << Lutaml::Model::Error.new("Degree settings for glass must be less than 1300")
end
end
end
kiln = Kiln.new(name: "Kiln", type: "glass", degree_settings: [100, 200, 1400])
kiln.validate
# => [#<Lutaml::Model::Error: Degree settings for glass must be less than 1300>]
Warning
|
The Liquid template feature is optional. To enable it, please
explicitly require the liquid gem.
|
The Liquid template language is an open-source template language developed by Shopify and written in Ruby.
Lutaml::Model::Serializable
objects can be safely accessed within Liquid
templates through a to_liquid
method that converts the objects into
Liquid::Drop
instances.
-
All attributes are accessible in the Liquid template by their names.
-
Nested attributes are also converted into
Liquid::Drop
objects so inner attributes can be accessed using the Liquid dot notation.
Note
|
Every Lutaml::Model::Serializable class extends the Liquefiable module
which generates a corresponding Liquid::Drop class.
|
Note
|
Methods defined in the Lutaml::Model::Serializable class are not
accessible in the Liquid template.
|
to_liquid
to convert model instances into corresponding Liquid drop instancesclass Ceramic < Lutaml::Model::Serializable
attribute :name, :string
attribute :temperature, :integer
end
ceramic = Ceramic.new({ name: "Porcelain Vase", temperature: 1200 })
ceramic_drop = ceramic.to_liquid
# Ceramic::CeramicDrop
puts ceramic_drop.name
# "Porcelain Vase"
puts ceramic_drop.temperature
# 1200
class Ceramic < Lutaml::Model::Serializable
attribute :name, :string
attribute :temperature, :integer
end
class CeramicCollection < Lutaml::Model::Serializable
attribute :ceramics, Ceramic, collection: true
end
sample.yml
:
---
ceramics:
- name: Porcelain Vase
temperature: 1200
- name: Earthenware Pot
temperature: 950
- name: Stoneware Jug
temperature: 1200
template.liquid
:
{% for ceramic in ceramic_collection.ceramics %}
* Name: "{{ ceramic.name }}"
** Temperature: {{ ceramic.temperature }}
{%- endfor %}
# Load the Lutaml::Model collection
ceramic_collection = CeramicCollection.from_yaml(File.read("sample.yml"))
# Load the Liquid template
template = Liquid::Template.parse(File.read("template.liquid"))
# Pass the Lutaml::Model collection to the Liquid template and render
output = template.render("ceramic_collection" => ceramic_collection)
puts output
# >
# * Name: "Porcelain Vase"
# ** Temperature: 1200
# * Name: "Earthenware Pot"
# ** Temperature: 950
# * Name: "Stoneware Jug"
# ** Temperature: 1200
class Glaze < Lutaml::Model::Serializable
attribute :color, :string
attribute :opacity, :string
end
class CeramicWork < Lutaml::Model::Serializable
attribute :name, :string
attribute :glaze, Glaze
end
class CeramicCollection < Lutaml::Model::Serializable
attribute :ceramics, Ceramic, collection: true
end
ceramic_work = CeramicWork.new({
name: "Celadon Bowl",
glaze: Glaze.new({
color: "Jade Green",
opacity: "Translucent"
})
})
ceramic_work_drop = ceramic_work.to_liquid
# CeramicWork::CeramicWorkDrop
puts ceramic_work_drop.name
# "Celadon Bowl"
puts ceramic_work_drop.glaze.color
# "Jade Green"
puts ceramic_work_drop.glaze.opacity
# "Translucent"
ceramics.yml
:
---
ceramics:
- name: Celadon Bowl
glaze:
color: Jade Green
opacity: Translucent
- name: Earthenware Pot
glaze:
color: Rust Red
opacity: Opaque
- name: Stoneware Jug
glaze:
color: Cobalt Blue
opacity: Transparent
templates/_ceramics.liquid
:
{% for ceramic in ceramic_collection.ceramics %}
{% render 'ceramic' ceramic: ceramic %}
{%- endfor %}
Note
|
render is a Liquid tag that renders a partial template, by default
Liquid uses the pattern _%s.liquid to find the partial template. Here
ceramic refers to the file at templates/_ceramic.liquid .
|
templates/_ceramic.liquid
:
* Name: "{{ ceramic.name }}"
** Temperature: {{ ceramic.temperature }}
{%- if ceramic.glaze %}
** Glaze (color): {{ ceramic.glaze.color }}
** Glaze (opacity): {{ ceramic.glaze.opacity }}
{%- endif %}
require 'liquid'
# Create a Liquid template object that supports dynamic loading
template = Liquid::Template.new
# Link the Liquid template object to a "local file system" (directory)
file_system = Liquid::LocalFileSystem.new('templates/')
template.registers[:file_system] = file_system
# Load the partial template, this is necessary.
# This will also allow Liquid to load any inner partials from the file system
# dynamically (see `file_system.pattern` to see what it loads)
template.parse(file_system.read_template_file('ceramics'))
# Read the lutaml-model collection
ceramic_collection = CeramicCollection.from_yaml(File.read("ceramics.yml"))
# Render the template with the collection
output = template.render("ceramic_collection" => ceramic_collection)
puts output
# >
# * Name: "Celadon Bowl"
# ** Temperature: 1200
# ** Glaze (color): Jade Green
# ** Glaze (finish): Translucent
# * Name: "Earthenware Pot"
# ** Temperature: 950
# ** Glaze (color): Rust Red
# ** Glaze (finish): Opaque
# * Name: "Stoneware Jug"
# ** Temperature: 1200
# ** Glaze (color): Cobalt Blue
# ** Glaze (finish): Transparent
Lutaml::Model uses an adapter pattern to support multiple libraries for each serialization format.
Lutaml::Model supports the following serialization formats:
-
XML (W3C XML Schema (Second Edition), XML 1.0)
-
YAML (YAML version 1.2)
-
JSON (ECMA-404 The JSON Data Interchange Standard, unofficial link: JSON)
-
TOML (TOML version 1.0)
-
You can also create custom adapters for additional data formats. See Custom Adapters Guide for detailed information.
You will need to specify the configuration for the adapter you want to use. The easiest way is to copy and paste the following configuration into your code.
The configuration is as follows:
require 'lutaml/model'
require 'lutaml/model/xml_adapter/nokogiri_adapter'
require 'lutaml/model/json_adapter/standard_json_adapter'
require 'lutaml/model/toml_adapter/toml_rb_adapter'
require 'lutaml/model/yaml_adapter/standard_yaml_adapter'
Lutaml::Model::Config.configure do |config|
config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
config.hash_adapter = Lutaml::Model::YamlAdapter::StandardHashAdapter
config.yaml_adapter = Lutaml::Model::YamlAdapter::StandardYamlAdapter
config.json_adapter = Lutaml::Model::JsonAdapter::StandardJsonAdapter
config.toml_adapter = Lutaml::Model::TomlAdapter::TomlRbAdapter
end
You can also provide the adapter type by using symbols like
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
config.xml_adapter_type = :nokogiri # can be one of [:nokogiri, :ox, :oga]
config.hash_adapter_type = :standard_hash
config.yaml_adapter_type = :standard_yaml
config.json_adapter_type = :standard_json # can be one of [:standard_json, :multi_json]
config.toml_adapter_type = :toml_rb # can be one of [:toml_rb, :tomlib]
end
Note
|
By default yaml_adapter_type and json_adapter_type are set to
:standard_yaml and :standard_json respectively.
|
Lutaml::Model supports the following XML adapters:
- Nokogiri
-
(default) Popular
libxml
based XML parser for Ruby. Requires native extensions (i.e. compiled C code). Requires thenokogiri
gem. - Oga
-
(optional) Pure Ruby XML parser. Does not require native extensions and is suitable for Opal (Ruby on JavaScript). Requires the
oga
gem. - Ox
-
(optional) Fast XML parser and object serializer for Ruby, implemented partially in C. Requires native extensions (i.e. compiled C code). Requires the
ox
gem.
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
require 'lutaml/model/xml_adapter/nokogiri_adapter'
config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
end
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
require 'lutaml/model/xml_adapter/oga_adapter'
config.xml_adapter = Lutaml::Model::XmlAdapter::OgaAdapter
end
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
require 'lutaml/model/xml_adapter/ox_adapter'
config.xml_adapter = Lutaml::Model::XmlAdapter::OxAdapter
end
Lutaml::Model supports only one YAML adapter.
- YAML
-
(default) The Psych YAML parser and emitter for Ruby. Included in the Ruby standard library.
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
require 'lutaml/model/yaml_adapter/standard_yaml_adapter'
config.yaml_adapter = Lutaml::Model::YamlAdapter::StandardYamlAdapter
end
Lutaml::Model supports the following JSON adapters:
- JSON
-
(default) The standard JSON library for Ruby. Included in the Ruby standard library.
- MultiJson
-
(optional) A gem that provides a common interface to multiple JSON libraries. Requires the
multi_json
gem.
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
require 'lutaml/model/json_adapter/standard_json_adapter'
config.json_adapter = Lutaml::Model::JsonAdapter::StandardJsonAdapter
end
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
require 'lutaml/model/json_adapter/multi_json_adapter'
config.json_adapter = Lutaml::Model::JsonAdapter::MultiJsonAdapter
end
Lutaml::Model supports the following TOML adapters:
- Toml-rb
-
(default) A TOML parser and serializer for Ruby that is compatible with the TOML v1.0.0 specification. Requires the
toml-rb
gem. - Tomlib
-
(optional) Toml-rb fork that is compatible with the TOML v1.0.0 specification, but with additional features. Requires the
tomlib
gem.
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
require 'lutaml/model/toml_adapter/toml_rb_adapter'
config.toml_adapter = Lutaml::Model::TomlAdapter::TomlRbAdapter
end
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
config.toml_adapter = Lutaml::Model::TomlAdapter::TomlibAdapter
require 'lutaml/model/toml_adapter/tomlib_adapter'
end
Lutaml::Model provides a flexible system for creating custom adapters to handle different data formats. For more information See Custom Adapters Guide
Lutaml::Model is a serialization library that is similar to Shale, but with some differences in implementation.
Feature | Lutaml::Model | Shale | Notes |
---|---|---|---|
Data model definition |
2 types:
|
||
Value types |
|
|
Lutaml::Model supports additional value types |
Configuration |
|
|
Lutaml::Model uses a configuration block to set the serialization adapters. |
Custom serialization methods |
|
|
Lutaml::Model uses the |
Serialization formats |
XML, YAML, JSON, TOML |
XML, YAML, JSON, TOML, CSV |
Lutaml::Model does not support CSV. |
Validation |
Supports collection range, fixed values, and custom validation |
Requires implementation |
|
Adapter support |
XML (Nokogiri, Ox, Oga), YAML, JSON (JSON, MultiJson), TOML (Toml-rb, Tomlib) |
XML (Nokogiri, Ox), YAML, JSON (JSON, MultiJson), TOML (Toml-rb, Tomlib), CSV |
Lutaml::Model does not support CSV. |
XML features |
|||
Yes. Supports |
No. Only supports |
||
XML mixed content support |
Yes. Supports the following kind of XML through mixed content support. <description>My name is
<bold>John Doe</bold>,
and I'm <i>28</i>
years old</description> |
No. Shale’s |
|
XML namespace inheritance |
Yes. Supports the |
No. |
|
Support for |
Yes. Automatically supports the |
Requires manual specification on every XML element that uses it. |
|
Compiling XML Schema to Lutaml::Model::Serializable classes |
Yes. Using
|
Yes, Provides only an array of the classes and doesn’t support |
|
Attribute features |
|||
Attribute delegation |
|
|
|
Enumerations |
Yes. Supports enumerations as value types through the
|
No. |
Lutaml::Model supports enumerations as value types. |
Attribute extraction |
Yes. Supports attribute extraction from key-value data models. |
No. |
Lutaml::Model supports attribute extraction from key-value data models. |
The following sections provide a guide for migrating from Shale to Lutaml::Model.
Lutaml::Model
uses Lutaml::Model::Serializable
as the base inheritance class.
class Example < Lutaml::Model::Serializable
# ...
end
Note
|
class Example
include Lutaml::Model::Serialize
# ...
end |
Shale uses Shale::Mapper
as the base inheritance class.
class Example < Shale::Mapper
# ...
end
Actions:
-
Replace mentions of
Shale::Mapper
withLutaml::Model::Serializable
. -
Potentially replace inheritance with inclusion for suitable cases.
Value types in Lutaml::Model
are under the Lutaml::Model::Type
module,
or use the LutaML type symbols.
class Example < Lutaml::Model::Serializable
attribute :length, :integer
attribute :description, :string
end
Note
|
class Example < Lutaml::Model::Serializable
attribute :length, Lutaml::Model::Type::Integer
attribute :description, "String"
end |
Value types in Shale are under the Shale::Type
module.
class Example < Shale::Mapper
attribute :length, Shale::Type::Integer
attribute :description, Shale::Type::String
end
Action:
-
Replace mentions of
Shale::Type
withLutaml::Model::Type
. -
Potentially replace value type definitions with strings or symbols.
Lutaml::Model
uses a configuration block to set the serialization adapters.
require 'lutaml/model/xml_adapter/nokogiri_adapter'
Lutaml::Model::Config.configure do |config|
config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
end
The equivalent for Shale is this:
require 'shale/adapter/nokogiri'
Shale.xml_adapter = Shale::Adapter::Nokogiri
Here are places that this code may reside at:
-
If your code is a standalone Ruby script, this code will be present in your code.
-
If your code is organized in a Ruby gem, this code will be specified somewhere referenced by
lib/your_gem_name.rb
. -
If your code contains tests or specs, they will be in the test setup file, e.g. RSpec
spec/spec_helper.rb
.
Actions:
-
Replace the Shale configuration block with the
Lutaml::Model::Config
configuration block. -
Replace the Shale adapter with the
Lutaml::Model
adapter.
There is an implementation difference between Lutaml::Model and Shale for custom serialization methods.
Custom serialization methods in Lutaml::Model
map to individual attributes.
For custom serialization methods, Lutaml::Model uses the :with
keyword
instead of the :using
keyword used by Shale.
class Example < Lutaml::Model::Serializable
attribute :name, :string
attribute :size, :integer
attribute :color, :string
attribute :description, :string
json do
map "name", to: :name, with: { to: :name_to_json, from: :name_from_json }
map "size", to: :size
map "color", to: :color,
with: { to: :color_to_json, from: :color_from_json }
map "description", to: :description,
with: { to: :description_to_json, from: :description_from_json }
end
xml do
root "CustomSerialization"
map_element "Name", to: :name,
with: { to: :name_to_xml, from: :name_from_xml }
map_attribute "Size", to: :size
map_element "Color", to: :color,
with: { to: :color_to_xml, from: :color_from_xml }
map_content to: :description,
with: { to: :description_to_xml,
from: :description_from_xml }
end
def name_to_json(model, doc)
doc["name"] = "JSON Masterpiece: #{model.name}"
end
def name_from_json(model, value)
model.name = value.sub(/^JSON Masterpiece: /, "")
end
def color_to_json(model, doc)
doc["color"] = model.color.upcase
end
def color_from_json(model, value)
model.color = value.downcase
end
def description_to_json(model, doc)
doc["description"] = "JSON Description: #{model.description}"
end
def description_from_json(model, value)
model.description = value.sub(/^JSON Description: /, "")
end
def name_to_xml(model, parent, doc)
el = doc.create_element("Name")
doc.add_text(el, "XML Masterpiece: #{model.name}")
doc.add_element(parent, el)
end
def name_from_xml(model, value)
model.name = value.sub(/^XML Masterpiece: /, "")
end
def color_to_xml(model, parent, doc)
color_element = doc.create_element("Color")
doc.add_text(color_element, model.color.upcase)
doc.add_element(parent, color_element)
end
def color_from_xml(model, value)
model.color = value.downcase
end
def description_to_xml(model, parent, doc)
doc.add_text(parent, "XML Description: #{model.description}")
end
def description_from_xml(model, value)
model.description = value.join.strip.sub(/^XML Description: /, "")
end
end
Custom serialization methods in Shale do not map to specific attributes, but allow the user to specify where the data goes.
class Example < Shale::Mapper
attribute :name, Shale::Type::String
attribute :size, Shale::Type::Integer
attribute :color, Shale::Type::String
attribute :description, Shale::Type::String
json do
map "name", using: { from: :name_from_json, to: :name_to_json }
map "size", to: :size
map "color", using: { from: :color_from_json, to: :color_to_json }
map "description", to: :description, using: { from: :description_from_json, to: :description_to_json }
end
xml do
root "CustomSerialization"
map_element "Name", using: { from: :name_from_xml, to: :name_to_xml }
map_attribute "Size", to: :size
map_element "Color", using: { from: :color_from_xml, to: :color_to_xml }
map_content to: :description, using: { from: :description_from_xml, to: :description_to_xml }
end
def name_to_json(model, doc)
doc['name'] = "JSON Masterpiece: #{model.name}"
end
def name_from_json(model, value)
model.name = value.sub(/^JSON Masterpiece: /, "")
end
def color_to_json(model, doc)
doc['color'] = model.color.upcase
end
def color_from_json(model, doc)
model.color = doc['color'].downcase
end
def description_to_json(model, doc)
doc['description'] = "JSON Description: #{model.description}"
end
def description_from_json(model, doc)
model.description = doc['description'].sub(/^JSON Description: /, "")
end
def name_from_xml(model, node)
model.name = node.text.sub(/^XML Masterpiece: /, "")
end
def name_to_xml(model, parent, doc)
name_element = doc.create_element('Name')
doc.add_text(name_element, model.street.to_s)
doc.add_element(parent, name_element)
end
end
Note
|
There are cases where the Shale implementation of custom methods work differently from the Lutaml::Model implementation. In these cases, you will need to adjust the custom methods accordingly. |
Actions:
-
Replace the
using
keyword with thewith
keyword. -
Adjust the custom methods.
The name "LutaML" is pronounced as "Looh-tah-mel".
The name "LutaML" comes from the Latin word for clay, "Lutum", and "ML" for "Markup Language". Just as clay can be molded and modeled into beautiful and practical end products, the Lutaml::Model gem is used for data modeling, allowing you to shape and structure your data into useful forms.
This project is licensed under the BSD 2-clause License. See the LICENSE.md file for details.
Copyright Ribose.