
Security News
rv Is a New Rust-Powered Ruby Version Manager Inspired by Python's uv
Ruby maintainers from Bundler and rbenv teams are building rv to bring Python uv's speed and unified tooling approach to Ruby development.
= LutaML Ruby modeller
image:https://img.shields.io/gem/v/lutaml-model.svg[RubyGems Version] image:https://img.shields.io/github/license/lutaml/lutaml-model.svg[License] image:https://github.com/lutaml/lutaml-model/actions/workflows/rake.yml/badge.svg["Build", link="https://github.com/lutaml/lutaml-model/actions/workflows/rake.yml"] image:https://github.com/lutaml/lutaml-model/actions/workflows/dependent-tests.yml/badge.svg["Dependent tests", link="https://github.com/lutaml/lutaml-model/actions/workflows/dependent-tests.yml"]
== Purpose
Lutaml::Model is the Ruby implementation of the LutaML modeling methodology, for:
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 https://www.shalerb.org[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 <>.
== Features
toml-rb
, tomlib
)== Data modeling in a nutshell
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
====
╔═══════════════════════╗ ╔════════════════════════════╗ ║ LutaML Core Model ║ ║ Serialization Models ║ ╚═══════════════════════╝ ╚════════════════════════════╝
╔═══════════════════════╗ ╔══════════════════╗ ╔═══════════════════════╗ ║LutaML Model Class FOO ║ ║LutaML Transformer║ ║LutaML Model Class BAR ║ ╚═══════════════════════╝ ╚══════════════════╝ ╚═══════════════════════╝
Value
class, transformation, and serialization formats
[source]╔═════════════════════╗ ╔═════════════════════╗ ╔═════════════════════╗ ║ Studio (Core Model) ║ ║ JSON Model ║ ║ Serialized JSON ║ ╚═════════════════════╝ ╚═════════════════════╝ ╚═════════════════════╝
====
== Installation
Add this line to your application's Gemfile:
And then execute:
Or install it yourself as:
== Components
LutaML provides the following set of components to model information in a structured way.
[[model-definition]] == Model
=== General
A LutaML model is used to represent a class of information, of which a model instance is a set of information representing a coherent concept.
There are two ways to define an information model in Lutaml::Model:
Lutaml::Model::Serializable
classLutaml::Model::Serialize
module=== Definition
[[define-through-inheritance]] ==== Through inheritance
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'
[[define-through-inclusion]] ==== Through inclusion
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
[[model-inheritance]] === Inheritance
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
end
class Subclass < Superclass
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
and key_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>>).
=== Comparison
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
[[value-definition]] == Value types
=== General types
Lutaml::Model supports the following attribute value types.
Every type has a corresponding Ruby class and a serialization format type.
.Mapping between Lutaml::Model::Type classes, Ruby equivalents and serialization format types |=== | Lutaml::Model::Type | Ruby class | XML | JSON | YAML | Example value
| :string
| String
| xs:string
| string
| string
| "text"
| :integer
| Integer
| xs:integer
| number
| integer
| 42
| :float
| Float
| xs:decimal
| number
| float
| 3.14
| :boolean
| TrueClass
/FalseClass
| xs:boolean
| boolean
| boolean
| true
, false
| :date
| Date
| xs:date
| string
| string
| 2024-01-01
(JSON/YAML "2024-01-01"
)
| :time_without_date
| Time
| xs:time
| string
| string
| "12:34:56"
| :date_time
| DateTime
| xs:dateTime
| string
| string
| "2024-01-01T12:00:00+00:00"
| :time
| Time
| xs:dateTime
| string
| string
| "2024-01-01T12:00:00+00:00"
| :decimal
(optional) | BigDecimal
| xs:decimal
| number
| float
| 123.45
| :hash
| Hash
| complex element | object | map | {key: "value"}
| (nil value) | nil
| xs:anyType
| null
| null
| null
// | class
| Custom class | complex element | object | map | CustomObject
// | collection: true
| Array
of type | repeated elements | array | sequence | [obj1, obj2]
// | any
|===
=== Decimal type
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:
If the bigdecimal
library is not loaded, usage of the Decimal
type will
raise a Lutaml::Model::TypeNotSupportedError
.
=== Custom type
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 as value
. 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
====
=== Serialization of custom types
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
self.cast(value)
and self.serialize(value)
methods2012-04-07T01:51:37.112+02:00
def self.from_xml(xml_string) ::DateTime.parse(xml_string) end
def to_xml value.strftime('%Y-%m-%dT%H:%M:%S.%L%:z') end end
An XML snippet with the high-precision date-time type:
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"}
====
[[attribute-definition]] == Attributes
=== Basic attributes
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:
Syntax:
Where,
name_of_attribute
:: The defined name of the attribute.
Type
:: The type of the attribute.
attribute
class method to define simple attributes
[example]====
==== Restricting the value of an attribute
The restrict
class method is used to update or refine the validation rules for an attribute that has already been defined. This allows you to apply additional or stricter constraints to an existing attribute without redefining it.
restrict
class method to update the options of an existing attribute
[example]====
class Document < Lutaml::Model::Serializable attribute :status, :string end
class DraftDocument < Document
restrict :status, values: %w[draft in_review] end
class PublishedDocument < Document
restrict :status, values: %w[published archived] end
Document
Document.new(status: "published").validate! # valid, there are no validation rules for Document
DraftDocument.new(status: "draft").validate! # valid
DraftDocument.new(status: "in_review").validate! # valid
DraftDocument.new(status: "published").validate! # raises error (not allowed)
PublishedDocument.new(status: "published").validate! # valid
PublishedDocument.new(status: "archived").validate! # valid
PublishedDocument.new(status: "draft").validate! # raises error (not allowed)====
All options that are supported by the attribute
class method are also supported by the restrict
method. Any unsupported option passed to restrict
will result in a Lutaml::Model::InvalidAttributeOptionsError
being raised.
=== Polymorphic attributes
==== General
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.
==== Naïve approach does not work...
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
class CeramicStudio < Studio attribute :clay_type, :string end
class PotteryClass < Lutaml::Model::Serializable
s = Studio.new(name: 'Pottery Studio') p = PotteryClass.new(studio: s) p.studio
s = CeramicStudio.new(name: 'Ceramic World', clay_type: 'Red') p = PotteryClass.new(studio: s) p.studio
p.studio.name
p.studio.clay_type
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
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
p.studio.clay_type
====
==== Proper polymorphic approaches
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:
====
In XML, the polymorphic class differentiator is typically an attribute that contains the polymorphic class name.
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:
.. <> .. <> .. <>
.. <> .. <> .. <>
NOTE: Please refer to spec/lutaml/model/polymorphic_spec.rb
for full examples
of implementing polymorphic attributes.
[[polymorphic-superclass-class]] ==== Defining the polymorphic attribute
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.
<1> The name of the polymorphic attribute.
<2> The polymorphic superclass class.
<3> Any options for the attribute.
<4> The polymorphic
option that determines the acceptable polymorphic subclasses, or just true
.
<5> The polymorphic subclasses.
The polymorphic
option is an array of polymorphic subclasses that the
attribute can accept.
These options enable the following scenarios.
polymorphic-superclass-class
, not its subclasses, then the polymorphic
option is not needed.In the following code, ReferenceSet
has an attribute references
that only
accepts instances of Reference
. The polymorphic
option does not apply.
====
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 attribute references
that only
accepts instances of DocumentReference
, a subclass of Reference
.
The polymorphic
option does not apply.
====
polymorphic: true
option.In the following code, ReferenceSet
is a class that has a polymorphic
attribute references
. The references
attribute can accept instances of
any polymorphic subclass of the Reference
base class, so polymorphic: true
is set.
====
polymorphic: [...]
option.In the following code, ReferenceSet
is a class that has a polymorphic
attribute references
. The references
attribute can accept instances of
DocumentReference
and AnchorReference
, both of which are subclasses of
Reference
.
====
[[polymorphic-subclass-differentiator]] ==== Differentiating polymorphic subclasses
===== General
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 <>).
Setting the polymorphic class differentiator in the individual polymorphic subclasses
[[polymorphic-differentiator-in-superclass]] ===== Setting the differentiator in the polymorphic superclass
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>
<1> The polymorphic differentiator is a normal attribute that can be
assigned to any name.
<2> The polymorphic differentiator must have a value type of :string
.
<3> The option for polymorphic_class
must be set to true
to indicate
that this attribute accepts subclass types.
[[polymorphic-differentiator-in-subclass]] ===== Setting the differentiator in the individual polymorphic subclasses
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:
class PolymorphicSuperclass < Lutaml::Model::Serializable
end
class PolymorphicSubclass < PolymorphicSuperclass attribute :{_polymorphic_differentiator}, <1> :string, <2> polymorphic_class: true <3>
<1> The polymorphic differentiator is a normal attribute that can be
assigned to any name.
<2> The polymorphic differentiator must have a value type of :string
.
<3> The option for polymorphic_class
must be set to true
to indicate
that this attribute accepts subclass types.
[[polymorphic-differentiator-in-serialization]] ==== Polymorphic differentiation in serialization
===== General
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:
[[polymorphic-attribute-class-mapping-in-superclass]] ===== Mapping in the polymorphic superclass
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, ]
<1> The name of the XML element or attribute that contains the polymorphic
differentiator.
<2> The name of the polymorphic differentiator attribute defined in attribute
with the polymorphic
option.
<3> The polymorphic_map
option that determines the class to use based on the
value of the differentiator.
<4> The mapping of the differentiator value to the polymorphic subclass.
<5> The name of the key-value element that contains the polymorphic
differentiator.
<6> The name of the polymorphic differentiator attribute defined in attribute
with the polymorphic
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
references:
[[polymorphic-attribute-class-mapping-in-subclasses]] ===== Mapping in the polymorphic attribute class and individual polymorphic subclasses
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:
_ref_type
, then it must be so in all other polymorphic subclasses.reference-type
, then it must
be so in the XML of all polymorphic subclasses.Syntax:
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
<1> The name of the XML element or attribute that contains the polymorphic
differentiator.
<2> The name of the key-value element that contains the polymorphic
differentiator.
<3> Definition of the polymorphic attribute and the polymorphic subclasses in
the polymorphic attribute class.
<4> The name of the XML element that contains the polymorphic attributes. This must
be an element as a polymorphic attribute must be a model.
<5> The polymorphic
option on a mapping defines necessary information for
polymorphic serialization.
<6> The attribute:
name of the polymorphic differentiator attribute defined in the
polymorphic subclass.
<7> The class_map:
option that determines the polymorphic subclass to use
based on the value of the differentiator.
<8> The name of the key-value format key that contains the polymorphic attributes.
<9> Same as <5>, but for the key-value format.
<10> Same as <6>, but for the key-value format.
<11> 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
references:
=== Collection attributes
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.
+
[example]0..1
, it means that the attribute is optional, it could be empty
or contain one object of the declared class.1..
(equivalent to 1..Infinity
), it means that the
attribute must contain at least one object of the declared class and can contain
any number of objects.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:
collection
option to define a collection attribute
[example]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']
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
puts SomeModel.new.to_xml
puts SomeModel.new.to_yaml
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
puts SomeModel.new.to_yaml
====
=== Derived attributes
A derived attribute has a value computed dynamically on evaluation of an instance method.
It is defined using the method:
option.
Syntax:
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
====
=== Choice attributes
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:
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
and choice
directives.
choice
directive to define a set of attributes with a range
[example]class 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
This means that the Studio
class must have at least one and at most three
attributes.
completeName
.prefix
and forename
attributes or just the forename
attribute.completeName
is optional.
====NOTE: The choice
directive can be used with import_model_attributes
. For more details, see <<import-model-attributes-inside-choice, Using import_model_attributes inside a choice block>>.
=== Importable models for reuse
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
parsed = GroupOfItems.from_xml(xml)
Lutaml::Model::NoRootMappingError: "GroupOfItems has
no_root
, it allowed only for reusable models"
====
[[import-model-mappings-inside-sequence]]
==== Using import_model_mappings
inside a sequence
You can use import_model_mappings
within a sequence
block to include the element mappings from another model. This is useful for composing complex XML structures from reusable model components.
The element mappings will be imported inside this specific sequence
block that calls the import method, rest of the mappings like content
, attributes
, etc. will be inserted at the class level.
NOTE: import_model
and import_model_attributes
are not supported inside a sequence
block.
class Address < Lutaml::Model::Serializable attribute :street, :string attribute :city, :string attribute :zip, :string
xml do no_root
map_element :street, to: :street
map_element :city, to: :city
map_element :zip, to: :zip
end end
class Person < Lutaml::Model::Serializable attribute :name, :string import_model_attributes Address
xml do root "Person"
map_element :name, to: :name
sequence do
import_model_mappings Address
end
end end
Element
zipdoes not match the expected sequence order element
city (Lutaml::Model::IncorrectSequenceError)
====
[[import-model-attributes-inside-choice]] ==== Using import_model_attributes inside a choice block
You can use import_model_attributes
within a choice
block to allow a model to accept one or more sets of attributes from other models, with flexible cardinality. This is especially useful when you want to allow a user to provide one or more alternative forms of information (e.g., contact methods) in your model.
For example, suppose you want a Person
model that can have either an email
, a phone
, or both as contact information. You can define ContactEmail
and ContactPhone
as importable models, and then use import_model_attributes
for both, inside a choice
block in the Person
model.
NOTE: The import_model_attributes
method is used to import the attributes from the other model into the current model. The imported attributes will be associated to the choice
block that calls the import method.
class ContactEmail < Lutaml::Model::Serializable attribute :email, :string
xml do no_root
map_element :email, to: :email
end end
class ContactPhone < Lutaml::Model::Serializable attribute :phone, :string
xml do no_root
map_element :phone, to: :phone
end end
class Person < Lutaml::Model::Serializable
choice(min: 1, max: 2) do import_model_attributes ContactEmail import_model_attributes ContactPhone end
xml do root "Person"
map_element :email, to: :email
map_element :phone, to: :phone
end end
valid_xml = <<~XML john.doe@example.com 1234567890 XML
Person.from_xml(valid_xml).validate! # #<Person:0x00000002d0e27fe8 @email="john.doe@example.com", @phone="1234567890">
invalid_xml = <<~XML XML
Lutaml::Model::ValidationError
error====
==== Using register functionality
The register functionality is useful when you want to reference or reuse a model by a symbolic name (e.g., across files or in dynamic scenarios), rather than by direct class reference.
Register
[example]The id: :group_of_items
assigns a symbolic name to the registered model, which can then be used in import_model :group_of_items
.
====
The import_model :group_of_items
will behave the same as import_model GroupOfItems
except the class is resolved from the provided register
.
NOTE: All the import_*
methods support the use of register
functionality.
NOTE: For more details on registers, see <<custom_registers, Custom Registers>>.
[[attribute-value-transform]] === Attribute value transform
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 <<mapping-value-transform,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).
number: 10.20
, unit: cm
."10.20 cm"
====The transform
option at the attribute
method is used to define a
transformation Proc
for the attribute value.
Syntax:
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.
c = Ceramic.new(name: "Celadon") c.name
"CELADON"
c.instance_attribute_get(:@name)
"Celadon"
Ceramic.new(name: "Celadon").name = "Raku"
"RAKU"
====
=== Value validation
==== General
There are several mechanisms to validate attribute values in Lutaml::Model.
[[attribute-enumeration]] ==== Values of an enumeration
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:
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 setvalue1?
: will return true if value is set, false otherwisevalue1=
: will set the value of name_of_attribute
equal to value1
if truthy value is given, and remove it otherwise.values
directive to define acceptable values for an attribute (basic types)
[example]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)
[example]class Ceramic < Lutaml::Model::Serializable attribute :type, :string attribute :firing_temperature, :integer 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
[example]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'
====
==== String values restricted to patterns
An attribute that accepts a string value accepts value validation using regular expressions.
Syntax:
pattern
option to restrict the value of an attribute
[example]In 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})$/
.
Glaze.new(color: '#ff0000').color
"#ff0000"
Glaze.new(color: '#ff000').color
Lutaml::Model::InvalidValueError: Invalid value for attribute 'color'
====
=== Attribute value defaults
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:
default
option to set a default value for an attribute
[example]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.
=== Attribute as raw string
An attribute can be set to read the value as raw string for XML, by using the raw: true
option.
Syntax:
raw
option to read raw value for an XML attribute
[example]For the following XML snippet:
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>
====
== Collections
=== General
Collections are used to represent a contained group of multiple instances of models.
Typically, a collection represents an "Array" or a "Set" in information modeling and programming languages. In LutaML, a collection represents an array of model instances.
Models in a collection may be:
constrained to be of a single kind;
constrained to be of multiple kinds sharing common characteristics;
unbounded of any kind.
LutaML Model provides the Lutaml::Model::Collection
class for defining
collections of model instances.
=== Configuration
==== All formats
The instances
directive defined at the Collection
class level is used to
define the collection attribute and the model type of the collection elements.
Syntax:
Where,
attribute
:: The name of the attribute that contains the collection.
ModelType
:: The model type of the collection elements.
==== Mapping instances: key-value formats only
The map_instances
directive is only used in the key_value
block.
Syntax:
class MyCollection < Lutaml::Model::Collection instances {attribute}, ModelType
Where,
attribute
:: The name of the attribute that contains model instances.
This directive maps individual array elements to the defined instances
attribute. These are the items considered part of the Collection and reflected
as Enumerable elements.
==== Mapping instances: XML only
In the xml
block, the map_element
, map_attribute
directives are used instead.
These directives map individual array elements to the defined instances
attribute. These are the items considered part of the Collection and reflected
as Enumerable elements.
Syntax for an element collection:
class MyCollection < Lutaml::Model::Collection instances {attribute}, ModelType
Where,
element-name
:: The name of the XML element of each model instance.
Syntax for an attribute collection:
Where,
attribute-name
:: The name of the XML attribute that contains all model instances.
=== Collection types
A LutaML collections is used for a number of scenarios:
=== Root collections (key-value formats only)
==== General
TODO: This case needs to be fixed for JSON.
A root collection is a collection that is not contained within a parent collection.
Root collections only apply to key-value serialization formats. The XML format does not support root collections.
NOTE: The https://www.w3.org/TR/xml11/[XML standard] mandates the existence of a non-repeated "root element" in an XML document. This means that a valid XML document must have a root element, and all elements in an XML document must exist within the root. This is why an XML document cannot be a "root collection".
NOTE: A root collection cannot be represented using a non-collection model.
Root collections store multiple instances of the same model type at the root level. In other words, these are model instances that do not have a defined container at the level of the LutaML Model.
There are two kinds of root collections depending on the type of the instance value:
"Root value collection":: the value is a "primitive type"
"Root object collection":: the value is a "model instance"
Regardless of the type of root collection, the instance in a collection is always a LutaML model instance.
==== Root value collections
A root value collection is a collection that directly contains values of a primitive type.
====
Syntax:
class MyCollection < Lutaml::Model::Collection instances :items, ModelType end
Code:
class Title < Lutaml::Model::Serializable attribute :content, :string end
class TitleCollection < Lutaml::Model::Collection instances :titles, Title
Data:
Usage:
titles = TitleCollection.from_yaml(yaml_data) titles.count
titles.first.content
====
==== Root object collections
A root object collection is a collection that directly contains model instances, each containing at least one serialized attribute.
name
[example]====
Code:
class Title < Lutaml::Model::Serializable attribute :content, :string end
class TitleCollection < Lutaml::Model::Collection instances :titles, Title
Data:
Usage:
titles = TitleCollection.from_yaml(yaml_data) titles.count
titles.first.content
====
=== Named collections
==== General
Named collections are collections wrapped inside a name or a key. The "name" of the collection serves as the container root of its contained model instances.
The named collection setup applies to XML and key-value serialization formats.
In a named collection setup, the collection is defined as a Lutaml::Model::Collection class, and each instance is defined as a Lutaml::Model::Serializable class.
There are two kinds of named collections depending on the type of the instance value:
"Named value collection":: the value is a "primitive type"
"Named object collection":: the value is a "model instance"
Regardless of the name of root collection, the instance in a collection is always a LutaML model instance.
==== Named value collections
A named value collection is a collection that contains values of a primitive type.
names:
Syntax:
class MyCollection < Lutaml::Model::Collection instances :items, ModelType
xml do root "name-of-xml-container-element" end
key_value do root "name-of-key-value-container-element" end end
A named collection can alternatively be implemented as a non-collection model ("Model class with an attribute") that contains the collection of instances. In this case, the attribute will be an Array object, which does not contain additional attributes and methods.
class Title < Lutaml::Model::Serializable attribute :title, :string
xml do root "title" map_content to: :title end end
class DirectTitleCollection < Lutaml::Model::Collection instances :items, Title
xml do root "titles" map_element "title", to: :items end
titles:
titles = DirectTitleCollection.from_yaml(yaml_data) titles.count
titles.first.title
titles.last.title
====
==== Named object collections
A named object collection is a collection that contains model instances, each containing at least one serialized attribute.
NOTE: A named object collection can alternatively be implemented as a non-collection model ("Model class with an attribute") that contains the collection of instances. In this case, the attribute will be an Array object, which does not contain additional attributes and methods.
names:
Data:
titles:
Code:
class Title < Lutaml::Model::Serializable attribute :title, :string
xml do root "title" map_element "content", to: :title end
key_value do map "title", to: :title end end
class TitleCollection < Lutaml::Model::Collection instances :items, Title
xml do root "titles" map_element 'title', to: :items end
Usage:
titles = TitleCollection.from_yaml(yaml_data) titles.count
titles.first.title
titles.last.title
====
=== Attribute collection class
A model attribute that is a collection can be contained within a custom collection class.
A custom collection class can be defined to provide custom behavior for the
collection inside a non-collection model, with attributes using
collection: true
.
Syntax:
class MyModel < Lutaml::Model::Serializable attribute {model-attribute}, ModelType, collection: MyCollection end
class MyCollection < Lutaml::Model::Collection instances {instance-name}, ModelType
Data:
titles:
class StringParts < Lutaml::Model::Collection instances :parts, :string
def to_s parts.join(' -- ') end end
class BibliographicItem < Lutaml::Model::Serializable attribute :title_parts, :string, collection: StringParts
xml do root "titles" map_element "title", to: :title_parts end
key_value do root "titles" map_instances to: :title_parts end
bib_item = BibliographicItem.from_xml(xml_data) bib_item.title_parts
StringParts:0x0000000104ac7240 @parts=["Title One", "Title Two", "Title Three"]
bib_item.render_title
"Title One -- Title Two -- Title Three"
====
=== Nested collections
TODO: This case needs to be fixed.
Collections can be nested within other models and define their own serialization rules.
Nested collections can be defined in the same way as root collections, but they are defined within the context of a parent model.
Data:
class Title < Lutaml::Model::Serializable attribute :content, :string end
class TitleCollection < Lutaml::Model::Collection instances :items, Title
xml do root "title-group" map_element "artifact", to: :items end end
class BibItem < Lutaml::Model::Serializable attribute :titles, TitleCollection
====
=== Keyed collections (key-value serialization formats only)
==== General
In key-value serialization formats, a key can be used to uniquely identify each instance. This usage allows for enforcing uniqueness in the collection.
A collection that contains keyed objects as its instances is commonly called a "keyed collection". A keyed object in a serialization format is an object identified with a unique key.
NOTE: The concept of keyed collections does not typically apply to XML collections.
There are two kinds of keyed collections depending on the type of the keyed value:
"keyed value collection":: the value is a "primitive type"
"keyed object collection":: the value is a "model instance"
Regardless of the type of keyed collections, the instance in a collection is always a LutaML model instance.
==== map_key
method
The map_key
method specifies that the unique key is to be moved into an
attribute belonging to the instance model.
Syntax:
Where,
to_instance
:: Refers to the attribute name in the instance that contains the key.
{key_attribute}
:: The attribute name in the instance that contains the key.
==== map_value
method
The map_value
method specifies that the value (the object referenced by the
unique key) is to be moved into an attribute belonging to the instance model.
Syntax:
key_value do
map_value {operation}: [*argument]
to_instance
map_value to_instance: {instance-attribute-name}
==== Keyed value collections
A keyed value collection is a collection where the keyed item in the serialization format is a primitive type (e.g. string, integer, etc.).
The instance item inside the collection is a model instance that contains both the serialized key and serialized value both as attributes inside the model.
All three map_key
, map_value
, and map_instances
methods need to be used to
define how instances are mapped in a keyed value collection.
class AuthorAvailability < Lutaml::Model::Serializable attribute :id, :string attribute :available, :boolean end
class AuthorCollection < Lutaml::Model::Collection instances :authors, AuthorAvailability
authors = AuthorCollection.from_yaml(yaml_data) authors.first.id
authors.first.available
====
==== Keyed object collections
A keyed object collection is a collection where the keyed item in the serialization format contains multiple attributes.
The instance item inside the collection is a model instance that contains the serialized key as one attribute, and the serialized value attributes are all attributes inside the model.
Both the map_key
and map_instances
are used to define how instances are
mapped in a keyed object collection.
class Author < Lutaml::Model::Serializable attribute :id, :string attribute :name, :string end
class AuthorCollection < Lutaml::Model::Collection instances :authors, Author
authors = AuthorCollection.from_yaml(yaml_data) authors.first.id
authors.first.name
====
==== Nested keyed object collection
A nested keyed object collection is a keyed collection that contain other keyed collections. This case is simply a more complex arrangement of the principles applied to keyed object collections.
This pattern can extend to multiple levels of nesting, where each level contains a keyed object collection that can have its own key and value mappings.
Depends on whether a custom collection class is needed, the following mechanisms are available:
When using a Lutaml::Model::Serializable class for a keyed collection,
use the child_mappings
option to map attributes.
When using a Lutaml::Model::Collection class for a keyed collection, there are two options:
use the map_key
, map_value
, and map_instances
methods to map attributes;
or
use the root_mappings
option to map attributes.
This example provides a two-layer nested structure where:
bowls
, vases
).class GlazeFinish < Lutaml::Model::Serializable attribute :name, :string attribute :temperature, :integer
key_value do map "name", to: :name map "temperature", to: :temperature end end
class CeramicPiece < Lutaml::Model::Serializable attribute :piece_type, :string attribute :glazes, GlazeFinish, collection: true
key_value do map "piece_type", to: :piece_type map "glazes", to: :glazes, child_mappings: { name: :key, temperature: :temperature } end end
class StudioInventory < Lutaml::Model::Collection instances :pieces, CeramicPiece
inventory = StudioInventory.from_yaml(yaml_data)
puts inventory.pieces.bowls.matte_finish.name
puts inventory.pieces.bowls.matte_finish.temperature
====
=== Behavior
==== Enumerable interface
Collections implement the Ruby Enumerable
interface, providing standard
collection operations.
Collections allow the following sample Enumerable
methods:
each
- Iterate over collection itemsmap
- Transform collection itemsselect
- Filter collection itemsfind
- Find items matching criteriareduce
- Aggregate collection itemsfiltered = collection.filter { |item| item.id == "1" }
rejected = collection.reject { |item| item.id == "1" }
selected = collection.select { |item| item.id == "1" }
mapped = collection.map { |item| item.name }
====
// ==== Collection validation
// Collections can define validation rules for their elements.
// [example] // ==== // [source,ruby] // ---- // class PublicationCollection < Lutaml::Model::Collection // instances(:publications, Publication) do // validates :year, numericality: { greater_than: 1900 }
// validate :must_have_author
// def must_have_author(publications)
// publications.each do |publication|
// next unless publication.author.nil?
// errors.add(:author, "#{publication.title}
must have an author")
// end
// end
// end
// end
// ----
// ====
==== Initialization
Collections can be initialized with an array of items or through individual item addition.
collection = ItemCollection.new
collection = ItemCollection.new([item1, item2, item3])
collection = ItemCollection.new([ { id: "1", name: "Item 1" }, { id: "2", name: "Item 2" } ])
====
==== Ordering
TODO: This case needs to be fixed.
Collections that maintain a specific ordering of elements.
Syntax:
Where,
{instances-name}
:: name of the instances accessor within the collection
ModelType
:: The model type of the collection elements.
{attribute-of-instance-or-proc}
:: How model instances are to be ordered by. Values supported are:
{attribute-of-instance}
::: Attribute name of an instance to be ordered by.
{proc}
::: Proc that returns a value to order by (same as sort_by
), given the instance as input.
order
::: Order direction of the value:
:asc
:::: Ascending order (default).
:desc
:::: Descending order.
Data:
class Item < Lutaml::Model::Serializable attribute :id, :string attribute :name, :string
xml do map_attribute "id", to: :id map_attribute "name", to: :name end end
class OrderedItemCollection < Lutaml::Model::Collection instances :items, Item ordered by: :id, order: :desc
xml do root "items" map_element "item", to: :items end
collection = OrderedItemCollection.from_xml(xml_data) collection.map(&:id)
["3", "2", "1"]
collection = OrderedItemCollection.from_yaml(yaml_data) collection.map(&:id)
["3", "2", "1"]
====
// ==== Polymorphic collections
// Collections can contain instances of different model classes that share a common // base class.
// The polymorphic options for attributes are also applied here.
// [example] // ==== // [source,ruby] // ---- // class PolymorphicItemCollection < Lutaml::Model::Collection // instances :items, Item, polymorphic: true
// xml do // root "items" // map_element "item", to: :items // end
// key_value do // root "items" // map_instances to: :items // end // end // ---- // ====
== Serialization model mappings
=== General
Lutaml::Model allows you to translate a data model into serialization models of various serialization formats.
Depending on the serialization format, different methods are supported for defining serialization and deserialization mappings.
A serialization model mapping is defined using a format-specific DSL block in this syntax:
<1> {format-short-name}
is the serialization format short name.
There are two kinds of serialization models:
A collection contains instances of singular models, and therefore is always inextricably linked to an underlying serialization format for singular models. For instance, JSONL represents a collection (itself being invalid JSON) that uses JSON for singular models.
The supported serialization formats and their short names are defined as follows:
Model serialization formats::
xml
::: XML
hsh
::: Hash
+
NOTE: Yes a 3-letter abbreviation for Hash!
json
::: JSON
yaml
::: YAML
toml
::: TOML
key_value
::: Key-value format, a shorthand for all key-value formats (including
JSON, YAML and TOML).
Collection serialization formats::
jsonl
::: JSONL (JSON Lines)
yamls
::: YAML Stream (multi-document format)
xml
, hsh
, json
, yaml
, toml
and key_value
blocks to define serialization mappings
[example]class Example < Lutaml::Model::Serializable xml do # ... end
hsh do # ... end
json do # ... end
yaml do # ... end
toml do # ... end
====
jsonl
block to define serialization mappings to a collection
[example]====
=== XML
==== Setting root element name
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]
Sets the tag name for <example>
in XML <example>...</example>
.
Syntax:
example
[example]Example.new.to_xml #
====
==== Ommiting root element
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:
class NameAndCode < Lutaml::Model::Serializable attribute :name, :string attribute :code, :string
ID-001
parsed = NameAndCode.from_xml(xml)
<NameAndCode:0x0000000107a3ca70 @code="ID-001", @name="Name">
parsed.to_xml
ID-001
Name
====
[[xml-map-all]] ==== Mapping all XML content
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 <>.
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:
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:
map_all
[example]class ExampleMapping < Lutaml::Model::Serializable attribute :description, :string
parsed = ExampleMapping.from_xml(xml) puts parsed.all_content
====
==== Mapping elements
The map_element
method maps an XML element to a data model attribute.
[example]
To handle the <name>
tag in <example><name>John Doe</name></example>
.
The value will be set to John Doe
.
Syntax:
name
tag to the name
attribute
[example]class Example < Lutaml::Model::Serializable attribute :name, :string
Example.from_xml(xml) #<Example:0x0000000104ac7240 @name="John Doe"> Example.new(name: "John Doe").to_xml #John Doe
====
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
RecordDate.new(date: "2021-01-01").to_xml #2021-01-01 OriginInfo.new(date_issued: [RecordDate.new(date: "2021-01-01")]).to_xml #2021-01-01
====
==== Mapping attributes
The map_attribute
method maps an XML attribute to a data model attribute.
Syntax:
map_attribute
to map the value
attribute
[example]The following class will parse the XML snippet below:
class Example < Lutaml::Model::Serializable attribute :value, :integer
Example.from_xml(xml) #<Example:0x0000000104ac7240 @value=12> Example.new(value: 12).to_xml #
====
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
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"/>
====
==== Mapping content
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:
map_content
to map content of the description
tag
[example]The following class will parse the XML snippet below:
class Example < Lutaml::Model::Serializable attribute :description, :string
Example.from_xml(xml) #<Example:0x0000000104ac7240 @description="John Doe is my moniker."> Example.new(description: "John Doe is my moniker.").to_xml #John Doe is my moniker.
====
==== CDATA nodes
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
on map_element
or map_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
on map_element
or map_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:
cdata
to map CDATA content
[example]The 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
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 #LutamlCareful
====
==== Example for mapping
The following class will parse the XML snippet below:
class Ceramic < Lutaml::Model::Serializable attribute :name, :string attribute :description, :string attribute :temperature, :integer
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 #Porcelain Vase with celadon glaze.
====
==== Namespaces
[[root-namespace]] ===== Namespace at root
The namespace
method in the xml
block sets the namespace for the root
element.
Syntax:
namespace
method to set the namespace for the root element
[example]class Ceramic < Lutaml::Model::Serializable attribute :type, :string attribute :glaze, :string
Ceramic.from_xml(xml_file) #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear"> Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml #PorcelainClear
====
namespace
method to set a prefixed namespace for the root element
[example]class Ceramic < Lutaml::Model::Serializable attribute :type, :string attribute :glaze, :string
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:TypePorcelain</cer:Type>cer:GlazeClear</cer:Glaze></cer:Ceramic>
====
===== Namespace on attribute
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:
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 element
[example]In 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
Using the original Glaze class namespace
Glaze.new(color: "Clear", temperature: 1050).to_xml #<glaze:Glaze xmlns="http://example.com/old_glaze">Clear1050</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 #Porcelain<glz:Glaze xmlns="http://example.com/glaze">Clear1050</glz:Glaze>
====
[[namespace-inherit]]
===== Namespace with inherit
option
The inherit
option is used at the element level to inherit the namespace from
the root element.
Syntax:
inherit
option to inherit the namespace from the root element
[example]In 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
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"
====
[[mixed-content]] ==== Mixed content
In XML there can be tags that contain content mixed with other tags and where whitespace is significant, such as to represent rich text.
My name is John Doe, and I'm 28 years old
====
To map this to Lutaml::Model we can use the mixed
option in either way:
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:
mixed
to treat root as mixed content
[example]class 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
Paragraph.from_xml("
My name is John Doe, and I'm 28 years old
") #<Paragraph:0x0000000104ac7240 @bold="John Doe", @italic="28"> Paragraph.new(bold: "John Doe", italic: "28").to_xml #My name is John Doe, and I'm 28 years old
====
// TODO: How to create mixed content from #new
?
[[ordered-content]] ==== Ordered content
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:
ordered
to treat root as ordered content
[example]class RootOrderedContent < Lutaml::Model::Serializable attribute :bold, :string attribute :italic, :string attribute :underline, :string
instance = RootOrderedContent.from_xml(xml) #<RootOrderedContent:0x0000000104ac7240 @bold="bell", @italic="384,400 km", @underline="Moon"> instance.to_xml #Moon384,400 kmbell
Without Ordered True:
class RootOrderedContent < Lutaml::Model::Serializable attribute :bold, :string attribute :italic, :string attribute :underline, :string
instance = RootOrderedContent.from_xml(xml) #<RootOrderedContent:0x0000000104ac7240 @bold="bell", @italic="384,400 km", @underline="Moon"> instance.to_xml #\n bell\n 384,400 km\n Moon\n
====
==== Sequence
The sequence
directive specifies that the defined attributes must appear in a
specified order in XML.
NOTE: Sequence only supports map_element
mappings.
Syntax:
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.
[example]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
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"> #]
Nick 1 Black Hard HERE > parsed = Kiln.from_xml(bad_xml) # => Lutaml::Model::ValidationError: Element 'name' is out of order in 'kiln' element ---- ====bad_xml = <<~HERE
NOTE: For importing model mappings inside a sequence
block, refer to <<import-model-mappings-inside-sequence, Importing model mappings inside a sequence
>>.
[[xml-schema-location]]
==== Automatic support of xsi:schemaLocation
The
https://www.w3.org/TR/xmlschema-1/#xsi_schemaLocation[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:
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 serialization
[example]In 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
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
====
NOTE: For details on xsi:schemaLocation
, please refer to the
https://www.w3.org/TR/xmlschema-1/#xsi_schemaLocation[W3C XML standard].
==== Character encoding
===== General
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 is UTF-8
.
** Oga: UTF-8
. The encoding is set to the default encoding of the Oga library, which
uses UTF-8
.
** Ox: ASCII-8bit
. The encoding is set to the default encoding of the Ox library, which uses
ASCII-8bit
.
When the encoding
option is not set, the default encoding of UTF-8
is
used.
===== Serialization character encoding (exporting)
====== General
There are two ways to set the character encoding of the XML document during serialization:
Instance setting::
Setting the instance-level encoding
option by setting
ModelClassInstance.encoding('...')
. This setting only affects serialization.
Per-export setting::
Setting the encoding
option when calling for serialization action using the
ModelClassInstance.to_xml(..., encoding: ...)
method.
[[encoding-instance-setting]] ====== Instance setting
The encoding
value of an instance sets the character encoding of the XML
document during serialization.
Syntax:
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
instance = JapaneseCeramic.new(glaze_type: "志野釉", description: "東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)") #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
instance.encoding = "Shift_JIS" #=> "Shift_JIS"
serialization_output = instance.to_xml #=> #\x{5FD8}\x{91CE}\x{91C9}\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}
serialization_output.encoding #=> "Shift_JIS"
====
====== Per-export setting
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:
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
ceramic_instance = Ceramic.new(potter: "John & Jane", description: " A ∑ series of ∏ porcelain µ vases.") #<Ceramic:0x0000000104ac7240 @potter="John & Jane", @description=" A ∑ series of ∏ porcelain µ vases.">
ceramic_parsed = Ceramic.from_xml(xml) #<Ceramic:0x0000000104ac7242 @potter="John & Jane", @description=" A ∑ series of ∏ porcelain µ vases.">
ceramic_parsed == ceramic_instance
true
ceramic_instance.to_xml #John & Jane A ∑ series of ∏ porcelain µ vases.
ceramic_instance.to_xml(encoding: nil) #John & Jane A ∑ series of ∏ porcelain µ vases.
ceramic_instance.to_xml(encoding: "ASCII") #John & Jane A ∑ series of ∏ porcelain µ vases.
====
to_xml
overrides instance encoding
[example]class JapaneseCeramic < Lutaml::Model::Serializable attribute :glaze_type, :string attribute :description, :string
instance = JapaneseCeramic.new(glaze_type: "志野釉", description: "東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)") #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
instance.encoding = "Shift_JIS" #=> "Shift_JIS"
serialization_output = instance.to_xml(encoding: "UTF-8") #=> #志野釉東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)
serialization_output.encoding #=> "UTF-8"
====
===== Deserialization character encoding (parsing)
The character encoding of the XML document being parsed is specified using the
encoding
option when the ModelClass.from_{format}(...)
is called.
Syntax:
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)
[example]Using the definition of JapaneseCeramic
at <>.
This XML snippet is in Shift-JIS.
instance = JapaneseCeramic.from_xml(xml, encoding: "Shift_JIS") #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
instance.encoding #=> "Shift_JIS"
serialization_output = instance.to_xml(encoding: "UTF-8") #=> #志野釉東京国立博物館コレクションの篠茶碗「橋本」(桃山時代) serialization_output.encoding #=> "UTF-8"
====
encoding
option is not set, the default encoding of the adapter is used
[example]Using the definition of JapaneseCeramic
at <>.
This XML snippet is in UTF-8.
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 #=> #志野釉東京国立博物館コレクションの篠茶碗「橋本」(桃山時代) 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 #=> #�菑��東京国立博物館コレクションの篠茶碗�橋本�桃山時代� serialization_output.encoding #=> "ASCII-8bit"
====
Using the definition of JapaneseCeramic
at <>.
This XML snippet is in UTF-8.
JapaneseCeramic.from_xml(xml, encoding: "Shift_JIS") #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="�菑���p���P", @description="�東京国立博物館コレクションの篠茶碗�橋本�桃山時代�">
====
=== Key value data models
==== General
Key-value data models share a similar structure where data is stored as key-value pairs.
Lutaml::Model
works with these formats in a similar way.
Key-value data models supported are identified by their short name:
hsh
:: Hash (Ruby Hash
class)
json
:: JSON
yaml
:: YAML
toml
:: TOML
key_value
:: A way to configure key-value mappings for all supported key-value data models.
==== Mapping
The map
method is used to define key-value mappings.
Syntax:
<1> key_value_type_short
is the key-value data model's short name.
====
====
==== Unified mapping
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 formats
[example]This 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
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 format mappings
Specific key value formats can be mapping independently of other formats.
map
method to define key-value mappings per format
[example]class 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
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}
====
[[key-value-map-all]] ==== Mapping all key-value content
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 <>.
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:
map_all
to capture all content across different formats
[example]class 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
sections:
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"
====
==== Nested attribute mappings
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
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}}
====
==== Collection with keyed elements (keyed collection)
===== General
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
[source,yaml]id
attribute value located inside each element
[source,yaml]There are key difference between these two data structures:
The <<collection-keyed-by-value,keyed object>> (first data structure) ensures
uniqueness of the id
attribute value across the collection, while the
<<collection-unkeyed-by-value,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
<1> The map
option indicates that this class represents the root of the
serialization object being passed in. The name_of_attribute
is the name
of the attribute that will hold the collection data. (Mandatory)
<2> The root_mappings
keyword specifies what the collection key represents and
and value for model. (Mandatory)
<3> The key
keyword specifies the attribute name of the individual collection
object type that represents its key used in the collection. (Mandatory)
<4> 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.)
<5> 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 <> 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.
===== Case 1: Only move the "key" into 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 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
<1> The :key
keyword specifies that the "key" of the keyed collection maps
to the value_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 models
[example]Given this data:
A model can be defined for this YAML as follows:
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
class CeramicCollection < Lutaml::Model::Serializable attribute :ceramics, Ceramic, collection: true
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
endceramic_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">]
id
attribute is: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 =>
ceramic_id
attribute is used to key the data,puts ceramic_collection.to_yaml =>
ceramic_id
attribute is used tomap
with root_mappings
method innew_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 =>
====
===== Case 2: Mapping the key
and complex value
s
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
<1> The :key
keyword specifies that the "key" of the keyed collection maps
to the value_type_attribute_name_for_key
attribute of the collection's
instance object (i.e. AttributeValueType
).
<2> The serialization_format_name_1
target specifies that the
serialization_format_name_2
key of the keyed collection value maps to the
value_type_attribute_name_for_value_data_1
attribute of the collection's
instance object.
<3> The [path name]
target specifies to fetch from [path name]
in the
serialization data model to be assigned to the
value_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
[example]A model can be defined for this YAML as follows:
class CeramicDetails < Lutaml::Model::Serializable attribute :name, :string attribute :insignia, :string
key_value do map 'name', to: :name map 'insignia', to: :insignia end end
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
class CeramicCollection < Lutaml::Model::Serializable attribute :ceramics, Ceramic, collection: true
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
endThe 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 =>
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
====
===== Case 3: Mapping the key
and delegating value
to an inner object
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
<1> The :key
keyword specifies that the "key" of the keyed collection maps
to the value_type_attribute_name_for_key
attribute of the collection's
instance object (i.e. AttributeValueType
).
<2> The :value
keyword specifies that the entire "value" of the keyed
collection maps to the value_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 models
[example]Given this data:
A model can be defined for this YAML as follows:
class CeramicDetails < Lutaml::Model::Serializable attribute :name, :string attribute :insignia, :string
key_value do map 'name', to: :name map 'insignia', to: :insignia end end
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
class CeramicCollection < Lutaml::Model::Serializable attribute :ceramics, Ceramic, collection: true
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
endceramic_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">]
id
attribute isfirst_ceramic = ceramic_collection.ceramics.first puts first_ceramic.to_yaml =>
ceramic_id
attribute is used to key the data,puts ceramic_collection.to_yaml =>
ceramic_id
attribute is used tomap
with root_mappings
method innew_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 =>
====
[[attribute-extraction]] ==== Attribute extraction
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
<1> The value_type_attribute_name_1
is the attribute name in the
AttributeValueType
model. The value of this attribute will be assigned the key
of the hash in the key-value data model.
<2> 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 the
AttributeValueType
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
.
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.
====
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 found
[example]In the following JSON content, the path_to_value
of [:extras, :sunroof]
and
[:extras, :drinks_cooler]
at the object "gearbox"
would be set to nil
.
====
child_mappings
option to extract values from a key-value data model
[example]The following JSON contains 2 keys in schema named foo
and bar
.
<1> The keys foo
and bar
are to be mapped to the id
attribute.
<2> The nested path.link
and path.name
keys are used as the link
and
name
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
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
and bar
) is mapped to the id
attribute.
The nested path.link
and path.name
keys are mapped to the link
and
name
attributes, respectively.
====
=== Collection data models
==== General
Collection data models represent a group of models, mapping to an instance of Lutaml::Model::Collection.
Collection data models supported are identified by their short name:
jsonl
:: JSONL (JSON Lines)
yamls
:: YAML Stream (multi-document format)
==== Mapping
As with collections in general, the map
method is used to define collection
mappings.
Syntax:
class MySerializedCollection < Lutaml::Model::Collection instances {attribute}, ModelType
Where,
{collection_type_short}
:: The short name of the collection type (e.g. jsonl
, yamls
).
{attribute}
:: The name of the attribute in the collection that will hold the
collection data.
ModelType
:: The type of the model that will be used in the collection.
A singular model may also utilize collection data models in the following manner.
Syntax:
class MySerializedCollection < Lutaml::Model::Serializeable attribute {attribute}, ModelType, collection: true
Where,
{collection_type_short}
:: The short name of the collection type (e.g. jsonl
, yamls
).
{attribute}
:: The name of the attribute in the collection that will hold the
collection data.
ModelType
:: The type of the model that will be used in the collection.
==== JSONL
JSONL (short for JSON Lines) is a serialization format where each line represents a valid JSON object. The format is meant to be efficient for large datasets such as for streaming or batch processing.
It represents a collection of JSON objects encoded one object per line.
NOTE: The contents of JSONL itself is not valid JSON, but each line is a valid JSON.
Since JSONL contains JSON elements, the model specified with instances
or
attribute
must support JSON.
Every line in a JSONL file is also a valid JSON object. If JSONL-specific
mappings (through jsonl
) are not defined in the model, the existing json
mappings are used instead as a fallback for serialization and deserialization.
class Person attribute :name, :string attribute :age, :integer attribute :id, :string end
class Directory < Lutaml::Model::Collection instances :persons, Person
jsonl do map_instances to: :persons end end
jsonl = <<~JSONL {"name":"John","age":30,"id":"abc-123"} {"name":"Jane","age":25,"id":"def-456"} JSONL
jsonl = Directory.from_jsonl(jsonl)
====
class Person attribute :name, :string attribute :age, :integer attribute :id, :string end
class Directory < Lutaml::Model::Serializeable attribute :persons, Person, collection: true
jsonl do map_instances to: :persons end end
jsonl = <<~JSONL {"name":"John","age":30,"id":"abc-123"} {"name":"Jane","age":25,"id":"def-456"} JSONL
jsonl = Directory.from_jsonl(jsonl)
====
class Person attribute :name, :string attribute :age, :integer attribute :id, :string
json do map "full_name", to: :name map "age", to: :age map "id", to: :id end end
class Directory < Lutaml::Model::Collection instances :persons, Person
jsonl do map_instances to: :persons end end
jsonl = <<~JSONL {"full_name":"John Doe","age":30,"id":"abc-123"} {"full_name":"Jane Smith","age":25,"id":"def-456"} JSONL
jsonl = Directory.from_jsonl(jsonl)
====
==== YAML Stream
YAML Stream (short for YAML multi-document format) is a serialization format
where each document is separated by a document separator (---
). The format is
meant to be efficient for large datasets such as for streaming or batch
processing.
It represents a collection of YAML documents encoded one document per stream.
NOTE: The contents of YAML Stream is valid YAML, where each document is a valid YAML document separated by document separators.
Since YAML Stream contains YAML elements, the model specified with instances
or attribute
must support YAML.
Every document in a YAML Stream file is also a valid YAML document. If YAML
Stream-specific mappings (through yamls
) are not defined in the model, the
existing yaml
mappings are used instead as a fallback for serialization and
deserialization.
class Person attribute :name, :string attribute :age, :integer attribute :id, :string end
class Directory < Lutaml::Model::Collection instances :persons, Person
yamls do map_instances to: :persons end end
name: Jane age: 25 id: def-456 YAMLS
yamls = Directory.from_yamls(yamls)
====
class Person attribute :name, :string attribute :age, :integer attribute :id, :string end
class Directory < Lutaml::Model::Serializeable attribute :persons, Person, collection: true
yamls do map_instances to: :persons end end
name: Jane age: 25 id: def-456 YAMLS
yamls = Directory.from_yamls(yamls)
====
class Person attribute :name, :string attribute :age, :integer attribute :id, :string
yaml do map "full_name", to: :name map "age", to: :age map "id", to: :id end end
class Directory < Lutaml::Model::Collection instances :persons, Person
yamls do map_instances to: :persons end end
full_name: Jane Smith age: 25 id: def-456 YAMLS
yamls = Directory.from_yamls(yamls)
====
=== Format-independent mechanisms
[[mapping-value-transform]] ==== Mapping value transformation
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-value-transform,attribute-level>> or at the serialization-mapping level. They can also be applied together.
Syntax:
class SomeObject < Lutaml::Model::Serializable
attribute :attribute_name, {attr_type}, transform: { <1> export: ->(value) { ... }, import: ->(value) { ... } }
{key_value_formats} do map "key", to: :attribute_name, transform: { <2> export: ->(value) { ... }, import: ->(value) { ... } } end
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) { ... }
}
<1> At the attribute level, the transform
option applied to the attribute
method is used to define the transformation for the attribute.
<2> At the mapping level (for {key_value_formats}
formats), the transform
option applied to the map
method is used to define the transformation for the
mapping.
<3> At the mapping level (for the XML format), the transform
option applied to
the map_*
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
json do map "glazeType", to: :glaze_type, transform: { export: ->(value) { "Traditional #{value}" }, import: ->(value) { value.gsub("Traditional ", "") } } end
ceramic = Ceramic.new(glaze_type: "celadon")
ceramic.to_json
ceramic.to_xml
ceramic.to_yaml
ceramic = Ceramic.from_json('{ "glazeType" => "Traditional celadon" }') ceramic.glaze_type
ceramic = Ceramic.from_xml('') ceramic.glaze_type
ceramic = Ceramic.from_yaml('glaze_type: "Traditional celadon"') ceramic.glaze_type
====
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.
class Ceramic < Lutaml::Model::Serializable
attribute :glaze_type, :string, transform: { export: ->(value) { "Ceramic #{value}" }, import: ->(value) { value.gsub("Ceramic ", "") } }
json do map "glazeType", to: :glaze_type, transform: { export: ->(value) { "Traditional #{value}" }, import: ->(value) { value.gsub("Traditional ", "") } } end
ceramic = Ceramic.new(glaze_type: "Ceramic celadon")
ceramic.glaze_type
ceramic.instance_value_get(:@glaze_type)
ceramic.to_json
ceramic.to_xml
ceramic.to_yaml
ceramic = Ceramic.from_json('{ "glazeType" => "Traditional Ceramic celadon" }') ceramic.glaze_type
ceramic = Ceramic.from_xml('') ceramic.glaze_type
ceramic = Ceramic.from_yaml('glaze_type: "Ceramic celadon"') ceramic.glaze_type
====
[[separate-serialization-model]] ==== Separate data model class
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}
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
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 #PorcelainClear
====
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
instance = CustomModelParentMapper.from_xml(xml) #<CustomModelParent:0x0000000107c9ca68 @child_mapper=#<CustomModelChild:0x0000000107c95218 @city="London", @street="Oxford Street">, @first_name="John"> CustomModelParentMapper.to_xml(instance) #<first_name>John</first_name>Oxford StreetLondon
====
==== Rendering default values (forced rendering of default values)
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
render_default
option to force encoding the default value
[example]class 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
====
render_default: true
are rendered when the value is identical to the default
[example]glaze_new = Glaze.new puts glaze_new.to_xml
puts glaze_new.to_json
====
render_default: true
with non-default values are rendered
[example]glaze = Glaze.new(color: 'Celadon', opacity: 'Semitransparent', temperature: 1300, firing_time: 90) puts glaze.to_xml
puts glaze.to_json
====
=== Advanced attribute mapping
==== Mapping multiple names to a single attribute
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
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
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
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
model = CustomModel.from_json(json) model.full_name
"Vase"
model.color
"blue"
====
==== Attribute mapping delegation
Delegate attribute mappings to nested objects using the delegate
option.
Syntax:
delegate
option to map attributes to nested objects
[example]The 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
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:
.
==== Attribute serialization with custom methods
===== General
Define custom methods for specific attribute mappings using the with:
key for
each serialization mapping block for from
and to
.
===== XML serialization with custom methods
Syntax:
with:
key to define custom serialization methods for XML
[example]The 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
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
def custom_method_from_xml(model, value) instance = value.node # Lutaml::Model::XmlAdapter::AdapterElement
instance = value.node.adapter_node # Adapter::Element
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
#<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656ed8
#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584f78
value.node.to_xml #Metadata
====
===== Key-value data model serialization with custom methods
with:
key to define custom serialization methods
[example]The 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
CustomCeramic.from_json(json) #<CustomCeramic:0x0000000104ac7240 @name="Vase", @size=12> CustomCeramic.new(name: "Vase", size: 12).to_json #{"name"=>"Masterpiece: Vase", "size"=>12}
====
=== Handling the missing values family
==== General
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.
.Support of missing value types in different technologies |=== | Technology | Missing value type | Realized as
.3+| Lutaml::Model
| empty value | Ruby empty string (""
)
| non-existent value | Ruby NilClass
(nil
)
| undefined value | class Uninitialized
.3+| XML element
| empty value | XML blank element: <status></status>
or <status/>
| non-existent value | XML blank element with attribute xsi:nil
: <status xsi:nil="true"/>
| undefined value | the XML element is omitted
.3+| XML attribute
| empty value | XML blank attribute: status=""
| non-existent value | the XML attribute is omitted
| undefined value | the XML attribute is omitted
.3+| JSON
| empty value | JSON empty string (""
)
| non-existent value | JSON null
value
| undefined value | the JSON key is omitted
.3+| 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.
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.
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>>.
[[value_representation_in_lutaml-model]] ==== Value representation in Lutaml::Model
The following table summarizes the behavior of the Lutaml::Model in regards of the "missing values" family.
.Handling of missing value types in Lutaml::Model data types [cols="1,1,2,2"] |=== | LutaML value type | Cardinality (1 or n) | Missing value type | Ruby value
.3+| Collection attribute
.3+| collection
| empty value | []
(Array)
| non-existent value | nil
(NilClass)
| undefined value | No assigned value
.3+| :string
.3+| single
| empty value | ""
(String)
| non-existent value | nil
(NilClass)
| undefined value | No assigned value
.3+| :integer
.3+| single
| empty value | N/A
| non-existent value | nil
(NilClass)
| undefined value | No assigned value
.3+| :float
.3+| single
| empty value | N/A
| non-existent value | nil
(NilClass)
| undefined value | No assigned value
.3+| :boolean
.3+| single
| empty value | N/A
| non-existent value | nil
(NilClass)
| undefined value | No assigned value
.3+| :date
.3+| single
| empty value | N/A
| non-existent value | nil
(NilClass)
| undefined value | No assigned value
.3+| :time_without_date
.3+| single
| empty value | N/A
| non-existent value | nil
(NilClass)
| undefined value | No assigned value
.3+| :date_time
.3+| single
| empty value | N/A
| non-existent value | nil
(NilClass)
| undefined value | No assigned value
.3+| :time
.3+| single
| empty value | N/A
| non-existent value | nil
(NilClass)
| undefined value | No assigned value
.3+| :decimal
.3+| single
| empty value | N/A
| non-existent value | nil
(NilClass)
| undefined value | No assigned value
.3+| :hash
.3+| single
| empty value | {}
(Hash)
| non-existent value | nil
(NilClass)
| undefined value | No assigned value
|===
[[value_representation_in_serialization_formats]] ==== Value representation in serialization formats
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.
.Varied handling of missing values in supported serialization formats |=== | Serialization format | Cardinality (1 or n) | Missing value type | Example
.6+| XML
.3+| collection
| empty collection | the XML blank element: <status></status>
or <status/>
| non-existent collection | a blank element with attribute xsi:nil
: <status xsi:nil="true"/>
| undefined collection | the XML element is omitted
.3+| single
| empty value | the XML blank element: <status></status>
or <status/>
| non-existent value | a blank element with attribute xsi:nil
: <status xsi:nil="true"/>
| undefined value | the XML element is omitted
.6+| JSON
.3+| collection
| empty collection | an empty array ([]
)
| non-existent collection | the value null
| undefined collection | the key is omitted
.3+| single
| empty value | an empty string (""
)
| non-existent value | the value null
| undefined value | the key is omitted
.6+| YAML
.3+| collection
| empty collection | an empty array ([]
)
| non-existent collection | the value null
| undefined collection | the key is omitted
.3+| single
| empty value | an empty string (""
)
| non-existent value | the value null
| undefined value | the key is omitted
.6+| TOML
.3+| collection
| empty collection | an empty array ([]
)
| non-existent collection | TOML does not support the concept of "null"
| undefined collection | the key is omitted
.3+| single
| empty value | an empty string (""
)
| non-existent value | TOML does not support the concept of "null"
| undefined value | the key is omitted
|===
==== Missing value mapping
===== General
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.
+
NOTE: In 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.
+
NOTE: In other words, used when converting a Lutaml::Model Ruby object into the
serialized format.
Syntax:
<1> The {map_command}
is a mapping rule with the actual command depending on
the serialization format.
<2> In the from
mapping, the keys are the missing value types in the
serialization format.
<3> 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.
.Available missing value types in different mapping commands
[cols="a,2a"]
|===
| Map command | Missing value types available (key in from:
direction, value in the to:
direction)
| XML element map_element
| :empty
, :omitted
, :nil
| XML attribute map_attribute
| :empty
, :omitted
| Hash map
, map_content
| :empty
, :omitted
, :nil
| JSON map
, map_content
| :empty
, :omitted
, :nil
| YAML map
, map_content
| :empty
, :omitted
, :nil
| TOML map
, map_content
| :empty
, :omitted
|===
[example]
For instance, TOML does not support the notion of "null" and therefore the
missing value type of 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.
====
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
and to
mappings are empty
,
omitted
, and nil
.
The values in the mappings can also be empty
, omitted
, and nil
.
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).
===== Default value maps for serialization formats
The table below describes the default value_map
configurations for supported
serialization formats.
// TODO: Find a place to write this
// XML Handling::
// - <status>new</status>
will be treated as ["new"]
.
// - <status>new</status><status>assigned</status>
will be treated as ["new", "assigned"]
.
====== Default value map for XML element (single attribute)
attribute :attr, :string
.Default missing value mapping configuration for single attributes in XML elements [cols="a,a,a,a"] |===
h| Direction h| Map rule h| XML source h| Model target
.3+|from:
| empty: :nil
| blank XML element (<status/>
)
| nil
| omitted: :omitted
| absent XML element
| omitted from the model
| nil: :nil
| blank XML element with attribute xsi:nil
(<status xsi:nil=true/>
)
| nil
value in the model
h| Direction h| Map rule h| Model source h| XML target
.3+|to:
| empty: :empty
| empty string (""
)
| blank XML element
| omitted: :omitted
| omitted in the model
| XML element not rendered
| nil: :nil
| nil
value in the model
| blank XML element with attribute xsi:nil
(<status xsi:nil=true/>
)
|===
====== Default value map for XML element (collection attribute)
attribute :attr, :string, collection: true
.Default missing value mapping configuration for collection attributes in XML elements [cols="a,a,a,a"] |===
h| Direction h| Map rule h| XML source h| Model target
.3+|from:
| empty: :nil
| blank XML element (<status/>
)
| empty array ([]
)
| omitted: :omitted
| absent XML element
| omitted from the model
| nil: :nil
| blank XML element with attribute xsi:nil
(<status xsi:nil=true/>
)
| nil
value in the model
h| Direction h| Map rule h| Model source h| XML target
.3+|to:
| empty: :empty
| empty array ([]
)
| blank XML element
| omitted: :omitted
| omitted in the model
| XML element not rendered
| nil: :nil
| nil
value in the model
| blank XML element with attribute xsi:nil
(<status xsi:nil=true/>
)
|===
====== Default value map for XML attribute (single attribute)
attribute :attr, :string
.Default missing value mapping configuration for single attributes in XML attributes [cols="a,a,a,a"] |===
h| Direction h| Map rule h| XML source h| Model target
.2+|from:
(source only supports empty
and omitted
)
| empty: :nil
| blank XML attribute (status=""
)
| nil
| omitted: :omitted
| absent XML attribute
| omitted from the model
h| Direction h| Map rule h| Model source h| XML target
.3+|to:
(target only accepts empty
and omitted
)
| empty: :empty
| empty string (""
)
| blank XML attribute (status=""
)
| omitted: :omitted
| omitted in the model
| XML attribute not rendered
| nil: :empty
| nil
| blank XML attribute (status=""
)
|===
====== Default value map for XML attribute (collection attribute)
attribute :attr, :string, collection: true
.Default missing value mapping configuration for collection attributes in XML attributes [cols="a,a,a,a"] |===
h| Direction h| Map rule h| XML source h| Model target
.2+|from:
(source only supports empty
and omitted
)
| empty: :empty
| blank XML attribute (status=""
)
| empty array ([]
)
| omitted: :omitted
| absent XML attribute
| omitted from the model
h| Direction h| Map rule h| Model source h| XML target
.3+|to:
(target only accepts empty
and omitted
)
| empty: :empty
| empty array ([]
)
| blank XML attribute (status=""
)
| omitted: :omitted
| omitted in the model
| XML attribute not rendered
| nil: :omitted
| nil
| XML attribute not rendered
|===
====== Default value map for YAML (single attribute)
attribute :attr, :string
.Default missing value mapping configuration for single attributes in YAML [cols="a,a,a,a"] |===
h| Direction h| Map rule h| XML source h| Model target
.3+|from:
| empty: :empty
| empty string in YAML (status:
or status: ""
)
| empty string (""
)
| omitted: :omitted
| absent YAML key
| omitted from the model
| nil: :nil
| YAML null
| nil
h| Direction h| Map rule h| Model source h| XML target
.3+|to:
| empty: :empty
| empty string (""
)
| empty string in YAML (status:
)
| omitted: :omitted
| omitted in the model
| YAML key omitted
| nil: :nil
| nil
| YAML null
|===
NOTE: In order to treat a YAML value like status: ''
to nil
, the mapping of
value_map: { from: { empty: :nil } }
can be applied.
====== Default value map for YAML (collection attribute)
attribute :attr, :string, collection: true
.Default missing value mapping configuration for collection attributes in YAML [cols="a,a,a,a"] |===
h| Direction h| Map rule h| YAML source h| Model target
.3+|from:
| empty: :empty
| empty YAML array (status:
or status: []
)
| empty array ([]
)
| omitted: :omitted
| absent YAML key
| omitted from the model
| nil: :nil
| YAML null
| nil
h| Direction h| Map rule h| Model source h| YAML target
.3+|to:
| empty: :empty
| empty array ([]
)
| empty YAML array (status: []
)
| omitted: :omitted
| omitted in the model
| YAML key omitted
| nil: :nil
| nil
| YAML null
|===
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.
====== Default value map for JSON (single attribute)
attribute :attr, :string
.Default missing value mapping configuration for single attributes in JSON [cols="a,a,a,a"] |===
h| Direction h| Map rule h| JSON source h| Model target
.3+|from:
| empty: :empty
| empty string in JSON ("status" : ""
)
| empty string (""
)
| omitted: :omitted
| absent JSON key
| omitted from the model
| nil: :nil
| JSON null
| nil
h| Direction h| Map rule h| Model source h| JSON target
.3+|to:
| empty: :empty
| empty string (""
)
| empty string in JSON ("status" : ""
)
| omitted: :omitted
| omitted in the model
| JSON key omitted
| nil: :nil
| nil
| JSON null
|===
====== Default value map for JSON (collection attribute)
attribute :attr, :string, collection: true
.Default missing value mapping configuration for collection attributes in JSON [cols="a,a,a,a"] |===
h| Direction h| Map rule h| JSON source h| Model target
.3+|from:
| empty: :empty
| empty JSON array ("status": []
)
| empty array ([]
)
| omitted: :omitted
| absent JSON key
| omitted from the model
| nil: :nil
| JSON null
| nil
h| Direction h| Map rule h| Model source h| JSON target
.3+|to:
| empty: :empty
| empty array ([]
)
| empty JSON array ("status": []
)
| omitted: :omitted
| omitted in the model
| JSON key omitted
| nil: :nil
| nil
| JSON null
|===
====== Default value map for TOML (single attribute)
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
.Default missing value mapping configuration for single attributes in TOML [cols="a,a,a,a"] |===
h| Direction h| Map rule h| TOML source h| Model target
.2+|from:
(source only supports empty
and omitted
)
| empty: :empty
| empty string in TOML ([status]
with no value)
| empty string (""
)
| omitted: :omitted
| absent TOML key
| omitted from the model
h| Direction h| Map rule h| Model source h| TOML target
.3+|to:
(source only supports empty
and omitted
)
| empty: :empty
| empty string (""
)
| empty string in TOML ([status]
with no value)
| omitted: :omitted
| omitted in the model
| TOML key omitted
| nil: :omitted
| nil
| TOML key omitted
|===
====== Default value map for TOML (collection attribute)
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
.Default missing value mapping configuration for collection attributes in TOML [cols="a,a,a,a"] |===
h| Direction h| Map rule h| TOML source h| Model target
.2+|from:
(source only supports empty
and omitted
)
| empty: :empty
| empty TOML array ([status]
with no value)
| empty array ([]
)
| omitted: :omitted
| absent TOML key
| omitted from the model
h| Direction h| Map rule h| Model source h| TOML target
.3+|to:
(source only supports empty
and omitted
)
| empty: :empty
| empty array ([]
)
| empty TOML array ([status]
with no value)
| omitted: :omitted
| omitted in the model
| TOML key omitted
| nil: :omitted
| nil
| TOML key omitted
|===
===== Replacing missing values type mapping with value_map
The value_map
option can be defined to meticulously map for each serialization
format as follows.
value_map
with from
and to
values
[source,ruby]class 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
status: '' YAML
ExampleClass.from_yaml(yaml)
YAML
ExampleClass.from_yaml(yaml1)
status: YAML
ExampleClass.from_yaml(yaml2)
====
When defining an attribute with collection: true
, the attribute will behave as follows:
Here's an example of how you can use the value_map
with a collection attribute.
value_map
with a collection attribute
[example]class 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
status: ['new', 'assigned'] YAML
y = ExampleClass.from_yaml(yaml)
====
// TODO: Need to improve this example.
==== Specific overrides of value map (render_*
and treat_*
)
===== General
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>
<1> The {map_command}
is a mapping rule with the actual command depending on
the serialization format. The attribute of attribute_name
may be a single or a collection value.
<2> The :render_*
mapping overrides the default value map for missing value types in model-to-serialization.
<3> 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.
By changing to this:
<1> This overrides the to:
direction nil: :nil
mapping.
<2> This overrides the from:
direction omitted: :omitted
mapping
The resulting value map would be:
nil: :nil
is now nil: :empty
.
<2> See that omitted: :omitted
is now omitted: :nil
==== render_nil
===== General
: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 is nil
. This has the same effect as render_nil: :as_empty
.
Syntax:
===== Render nil
as true
render_nil: true
option to render an attribute value of nil
as an empty element
[example]class 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
Ceramic.new.to_json
{ 'type': null }
Ceramic.new(type: "Porcelain", glaze: "Clear").to_json
{ 'type': 'Porcelain', 'glaze': 'Clear' }
Ceramic.new.to_xml
Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
PorcelainClear
====
render_nil: true
option to render an empty attribute collection of nil
as an empty element
[example]class 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
Ceramic.new.to_json
{ 'type': null, 'glazes': [] }
Ceramic.new(type: "Porcelain", glazes: ["Clear"]).to_json
{ 'type': 'Porcelain', 'glazes': ['Clear'] }
Ceramic.new.to_xml
Ceramic.new(type: "Porcelain", glazes: ["Clear"]).to_xml
PorcelainClear
====
===== Render nil
as omit
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
puts SomeModel.new.to_xml
puts SomeModel.new.to_yaml
====
===== Render nil
as nil
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
puts SomeModel.new.to_xml
puts SomeModel.new.to_yaml
====
===== Render nil
as blank
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
puts SomeModel.new.to_xml
puts SomeModel.new.to_yaml
====
==== render_empty
===== General
: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:
===== Render empty
as omit
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
puts SomeModel.new.to_yaml
====
===== Render empty
as nil
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
puts SomeModel.new.to_yaml
====
===== Render empty
as blank
/empty
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
puts SomeModel.new.to_yaml
====
== Schema generation and import
=== Schema generation
Lutaml::Model provides functionality to generate schema definitions from LutaML models. This allows you to create schemas that can be used for validation or documentation purposes.
Currently, the following schema formats are supported:
==== JSON Schema generation
The Lutaml::Model::Schema.to_json
method generates a JSON Schema from a LutaML model class. The generated schema includes:
Example:
class Glaze < Lutaml::Model::Serializable attribute :color, :string attribute :finish, :string end
class Vase < Lutaml::Model::Serializable attribute :height, :float attribute :diameter, :float attribute :glaze, Glaze attribute :materials, :string, collection: true end
schema = Lutaml::Model::Schema.to_json( Vase, id: "https://example.com/vase.schema.json", description: "A vase schema", pretty: true )
The generated schema will include definitions for all nested models and their attributes.
==== YAML Schema generation
The Lutaml::Model::Schema.to_yaml
method generates a YAML Schema from a LutaML model class. The generated schema includes the same features as the JSON Schema generation.
Example:
class Glaze < Lutaml::Model::Serializable attribute :color, :string attribute :finish, :string end
class Vase < Lutaml::Model::Serializable attribute :height, :float attribute :diameter, :float attribute :glaze, Glaze attribute :materials, :string, collection: true end
schema = Lutaml::Model::Schema.to_yaml( Vase, id: "http://example.com/schemas/vase", description: "A vase schema", pretty: true )
=== Importing data models
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-to-model-files]] === XML Schema (XSD)
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:
<1> The xsd_schema
is the XML Schema string to be converted to model files.
<2> 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.
+
[example]
"path/to/directory"
create_files
::: A boolean
argument (false
by default) to create files directly in the specified directory as defined by the output_dir
option.
+
[example]
create_files: (true | false)
load_classes
::: A boolean
argument (false
by default) to load generated classes before returning them.
+
[example]
load_classes: (true | false)
namespace
::: The namespace of the schema. This will be added in the
Lutaml::Model::Serializable
file's xml do
block.
+
[example]
http://example.com/namespace
prefix
::: The prefix of the namespace provided in the namespace
option.
+
[example]
example-prefix
location
::: The URL or path of the directory containing all the files of the
schema. For more information, refer to the
link:https://www.w3.org/TR/xmlschema-1/#include[XML Schema specification].
+
[example]
"http://example.com/example.xsd"
+
[example]
"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 https://github.com/lutaml/lutaml-xsd[`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 = {
output_dir: 'path/to/directory', namespace: 'http://example.com/namespace', prefix: "example-prefix", location: "http://example.com/example.xsd",
create_files: true, # Default: false
load_classes: true, # Default: false }
====
You could also directly load the generated Ruby files into your application by requiring them.
====
== Validation
=== General
Lutaml::Model provides a way to validate data models using the validate
and
validate!
methods.
The validate
method sets an errors
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 a Lutaml::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 rangeThe 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 Klin < 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
klin = Klin.new(name: "Klin", degree_settings: [100, 200, 300], description: "one", prefix: "Ben") klin.validate
klin = Klin.new(name: "Klin", degree_settings: [], description: "four", prefix: "Ben", nick_name: "Smith") klin.validate
e = klin.validate!
e.errors
====
=== Custom validation
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 Klin < 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
klin = Klin.new(name: "Klin", type: "glass", degree_settings: [100, 200, 1400]) klin.validate
====
== Liquid template access
WARNING: The Liquid template feature is optional. To enable it, please
explicitly require the liquid
gem.
The https://shopify.github.io/liquid/[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.
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.
.Using to_liquid
to convert model instances into corresponding Liquid drop instances
class Ceramic < Lutaml::Model::Serializable attribute :name, :string attribute :temperature, :integer end
ceramic = Ceramic.new({ name: "Porcelain Vase", temperature: 1200 }) ceramic_drop = ceramic.to_liquid
puts ceramic_drop.name
puts ceramic_drop.temperature
====
class Ceramic < Lutaml::Model::Serializable attribute :name, :string attribute :temperature, :integer end
sample.yml
:
ceramics:
template.liquid
:
{% for ceramic in ceramic_collection.ceramics %}
ceramic_collection = CeramicCollection.from_yaml(File.read("sample.yml"))
template = Liquid::Template.parse(File.read("template.liquid"))
output = template.render("ceramic_collection" => ceramic_collection) puts output
====
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
puts ceramic_work_drop.name
puts ceramic_work_drop.glaze.color
puts ceramic_work_drop.glaze.opacity
ceramics.yml
:
ceramics:
templates/_ceramics.liquid
:
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
:
require 'liquid'
template = Liquid::Template.new
file_system = Liquid::LocalFileSystem.new('templates/') template.registers[:file_system] = file_system
file_system.pattern
to see what it loads)template.parse(file_system.read_template_file('ceramics'))
ceramic_collection = CeramicCollection.from_yaml(File.read("ceramics.yml"))
output = template.render("ceramic_collection" => ceramic_collection) puts output
====
== Serialization adapters
=== General
The LutaML component that serializes a model into a serialization format is called an adapter. A serialization format may be supported by multiple adapters.
An adapter typically:
LutaML, out of the box, supports the following serialization formats:
The adapter interface is also used to support certain transformation of models
into an "end format", which is not a serialization format. For example, the
Lutaml::Model::HashAdapter
is used to convert a model into a hash format
that is not a serialization format.
Users can extend LutaML by creating custom adapters for other serialization formats or for other data formats. The link:docs/custom_adapters.adoc[Custom Adapters Guide] describes this process in detail.
For certain serialization formats, LutaML provides multiple adapters to support different serialization libraries. Please refer to their specific sections for more information.
=== Configuration
==== General
It is necessary to configure the adapter to be used for serialization and deserialization for a set of formats that the LutaML models will be transformed into.
There are two cases where you need to define such configuration:
End-user usage of the LutaML models. This is the case where you are using LutaML models in your application and want to serialize them into a specific format. If you are a gem developer that relies on lutaml-model, this case does not apply to you, because the end-user of your gem should determine the adapter configuration.
Testing purposes, e.g. RSpec. In order to run tests that involve verifying correctness of serialization, it is necessary to define adapter configuration.
There are two ways to specify a configuration:
There is a default configuration for adapters for commonly used formats:
yaml_adapter_type
is set to :standard_yaml
json_adapter_type
is set to :standard_json
hash_adapter_type
is set to :standard_hash
==== Configure adapters through symbol choices
The end-user or a gem developer can copy and paste the following configuration into an early loading file in their application or gem.
This configuration is preferred over the class choices because it is more
concise and does not require any require
code specific to the internals
of the LutaML runtime implementation.
Syntax:
require 'lutaml/model'
==== Configure adapters through class choices
The end-uesr or a gem developer can copy and paste the following configuration into an early loading file in their application or gem.
Only the serialization formats used will require a configuration.
Syntax:
require 'lutaml/model' require 'lutaml/model/xml/nokogiri_adapter' require 'lutaml/model/hash_adapter/standard_adapter' require 'lutaml/model/json/standard_adapter' require 'lutaml/model/yaml/standard_adapter' require 'lutaml/model/toml/toml_rb_adapter'
====
=== XML
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 the nokogiri
gem.
Oga::
(optional)
Pure Ruby XML parser.
Does not require native extensions and is suitable for
https://opalrb.com[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| config.xml_adapter = :nokogiri
config.xml_adapter = :oga
=== YAML
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'
=== JSON
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| config.json_adapter = :standard_json
=== TOML
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| config.toml_adapter = :toml_rb
[[custom-adapters]] == Custom serialization adapters
Lutaml::Model provides a flexible system for creating custom adapters to handle different data formats.
Please refer to link:docs/custom_adapters.adoc[Custom adapters] for details and examples.
[[schema-generation]] == Schema generation
Lutaml::Model provides functionality to generate schema definitions from LutaML models. This allows you to create schemas that can be used for validation or documentation purposes.
Currently, the following schema formats are supported:
The schema generation supports advanced features such as:
Please refer to link:docs/schema_generation.adoc[Schema Generation] for details and examples.
[[schema-import]] == Schema import
Lutaml::Model provides functionality to import schema definitions into LutaML models. This allows you to create models from existing schema definitions.
Currently, the following schema formats are supported:
Please refer to link:docs/schema_import.adoc[Schema Import] for details and examples.
[[custom-registers]] === Custom Registers
A LutaML::Model Register allows for dynamic modification and reconfiguration of model hierarchies without altering the original model definitions. For more information, refer to the link:docs/custom_registers.adoc[Custom Registers Guide].
NOTE: Before using the Lutaml::Model::Register
instance, make sure to register it in Lutaml::Model::GlobalRegister
.
NOTE: By default, a default_register
with the id :default
is created and registered in the GlobalRegister. This default register is also set in Lutaml::Model::Config.default_register
as the default value.
The default register can be set at the configuration level using the following syntax:
Lutaml::Model::Config.default_register = :default # the register id goes here.
== Comparison with Shale
Lutaml::Model is a serialization library that is similar to Shale, but with some differences in implementation.
[cols="a,a,a,a",options="header"] |=== | Feature | Lutaml::Model | Shale | Notes
| Data model definition | 3 types:
<<define-through-inheritance,Inherit from Lutaml::Model::Serializable
>>
<<define-through-inclusion,Include Lutaml::Model::Serialize
>>
<<separate-serialization-model,Separate serialization model class>> | 2 types:
Inherit from Shale::Mapper
Custom model class |
| Value types
| Lutaml::Model::Type
includes: Integer
, String
, Float
, Boolean
, Date
, DateTime
, Time
, Decimal
, Hash
.
| Shale::Type
includes: Integer
, String
, Float
, Boolean
, Date
, Time
.
| Lutaml::Model supports additional value types Decimal
, DateTime
and Hash
.
| Configuration
| Lutaml::Model::Config
| Shale.{type}_adapter
| Lutaml::Model uses a configuration block to set the serialization adapters.
| Custom serialization methods
| :with
, on individual attributes
| :using
, on entire object/document
| Lutaml::Model uses the :with
keyword for custom serialization methods.
| 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.
4+h| XML features
| <<root-namespace,XML default namespace>>
| Yes. Supports <root xmlns='http://example.com'>
through the namespace
option without prefix.
| No. Only supports <root xmlns:prefix='http://example.com'>
.
|
| XML mixed content support | Yes. Supports the following kind of XML through <<mixed-content,mixed content>> support.
| No. Shale's map_content
only supports the first text node.
|
| XML namespace inheritance
| Yes. Supports the <<namespace-inherit,inherit
>> option to inherit the
namespace from the root element.
| No.
|
| Support for xsi:schemaLocation
| Yes. Automatically supports the <<xml-schema-location,xsi:schemaLocation
>>
attribute for every element.
| Requires manual specification on every XML element that uses it.
|
| Compiling XML Schema to Lutaml::Model::Serializable classes
| Yes. Using <<xml-schema-to-model-files, Lutaml::Model::Schema#from_xml
>>
simple types
with restrictions and/or other validations.
|4+h| Attribute features
| Attribute delegation
| :delegate
option to delegate attribute mappings to a model.
| :receiver
option to delegate attribute mappings to a model.
|
| Enumerations
| Yes. Supports enumerations as value types through the
<<attribute-enumeration,values:
option>>.
| No.
| Lutaml::Model supports enumerations as value types.
| Attribute extraction | Yes. Supports <<attribute-extraction,attribute extraction>> from key-value data models. | No. | Lutaml::Model supports attribute extraction from key-value data models.
| Register
| Yes. Supports three types of registers(<<custom-register, read more details>>) with different types of functionalities.
| Supports register
functionality for Shale::Type classes only.
| Lutaml::Model
registers both Registrable
classes and Lutaml::Model::Type
classes, offering a more comprehensive registration system.
|===
[[migrate-from-shale]] == Migration steps from Shale
The following sections provide a guide for migrating from Shale to Lutaml::Model.
=== Step 1: Replace inheritance class
Lutaml::Model
uses Lutaml::Model::Serializable
as the base inheritance class.
class Example < Lutaml::Model::Serializable
Lutaml::Model
also supports an inclusion method as in the following example,
which is not supported by Shale. This is useful for cases where you want to
include the serialization methods in a class that already inherits from another
class.
class Example include Lutaml::Model::Serialize
====
Shale uses Shale::Mapper
as the base inheritance class.
class Example < Shale::Mapper
Actions:
Shale::Mapper
with Lutaml::Model::Serializable
.=== Step 2: Replace value type definitions
Value types in Lutaml::Model
are under the Lutaml::Model::Type
module,
or use the LutaML type symbols.
Lutaml::Model
supports specifying predefined value types as strings or
symbols, which is not supported by Shale.
====
Value types in Shale are under the Shale::Type
module.
Action:
Shale::Type
with Lutaml::Model::Type
.=== Step 3: Configure serialization adapters
Lutaml::Model
uses a configuration block to set the serialization adapters.
The equivalent for Shale is this:
Here are places that this code may reside at:
lib/your_gem_name.rb
.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.
=== Step 4: Rewrite custom serialization methods
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
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
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:
using
keyword with the with
keyword.== About LutaML
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.
== License and Copyright
This project is licensed under the BSD 2-clause License. See the link:LICENSE.md[] file for details.
Copyright Ribose.
FAQs
Unknown package
We found that lutaml-model demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Ruby maintainers from Bundler and rbenv teams are building rv to bring Python uv's speed and unified tooling approach to Ruby development.
Security News
Following last week’s supply chain attack, Nx published findings on the GitHub Actions exploit and moved npm publishing to Trusted Publishers.
Security News
AGENTS.md is a fast-growing open format giving AI coding agents a shared, predictable way to understand project setup, style, and workflows.