= LutaML Ruby modeller
https://github.com/lutaml/lutaml-model[image:https://img.shields.io/github/stars/lutaml/lutaml-model.svg?style=social[GitHub Stars]]
https://github.com/lutaml/lutaml-model[image:https://img.shields.io/github/forks/lutaml/lutaml-model.svg?style=social[GitHub Forks]]
image:https://img.shields.io/github/license/lutaml/lutaml-model.svg[License]
image:https://img.shields.io/github/actions/workflow/status/lutaml/lutaml-model/test.yml?branch=main[Build Status]
image:https://img.shields.io/gem/v/lutaml-model.svg[RubyGems Version]
== Purpose
Lutaml::Model is the Ruby implementation of the LutaML modeling methodology,
for:
- creating information models in the LutaML language (or its Ruby DSL)
- serializing and deserializing LutaML information models
- accessing data instances of LutaML information models
- documenting LutaML information models
It provides simple, flexible and comprehensive mechanisms for defining
information models with attributes and types, and the serialization of them
to/from serialization formats including 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
- Define models with attributes and types
- Serialize and deserialize models to/from JSON, XML, YAML, and TOML
- Support for multiple serialization libraries (e.g.,
toml-rb
, tomlib
) - Configurable adapters for different serialization formats
- Support for collections and default values
- Custom serialization/deserialization methods
- XML namespaces and mappings
== 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:
.Modeling relationships of a LutaML Model
[source]
LutaML Model
│
Has many attributes
│
▼
Attribute
│
Has type of
│
┌──────────┴──────────┐
│ │
Model Value (Leaf)
│ │
Has many attributes Contains one basic value
│ │
┌───────┴─────┐ ┌──────┴──────┐
│ │ │ │
Model Value (Leaf) String Integer
│ Date Boolean
│ Time Float
Has many attributes ... ...
│
▼
(Recursive pattern continues...)
.Example of LutaML Model instance with assigned values
[source]
Studio (Model)
├── name (Value: String) = "Pottery Studio"
├── address (Model)
│ ├── street (Value: String) = "123 Clay St"
│ ├── city (Value: String) = "Ceramics City"
│ └── postcode (Value: String) = "12345"
├── established (Value: Date) = 2020-01-01
└── kilns (Model)
├── count (Value: Integer) = 3
└── temperature (Value: Float) = 1200.0
====
.Modeling relationships of a LutaML Model to serialization models
[source]
╔═══════════════════════╗ ╔════════════════════════════╗
║ LutaML Core Model ║ ║ Serialization Models ║
╚═══════════════════════╝ ╚════════════════════════════╝
╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆ Model ┆ ┆ XML Model ┆
┆ │ ┆ ┌────────────────┐ ┆ │ ┆
┆ ┌────────┴──┐ ┆ │ │ ┆ ┌──────┴──────┐ ┆
┆ │ │ ┆ │ Model │ ┆ │ │ ┆
┆ Models Value Types ┆──►│ Transformation │ ┆ Models Value Types ┆
┆ │ │ ┆ │ & │ ┆ │ │ ┆
┆ │ │ ┆ │ Mapping Rules │ ┆ │ │ ┆
┆ │ ┌──────┴──┐ ┆ │ │ ┆ ┌────┴────┐ ┌─┴─┐ ┆
┆ │ │ │ ┆ └────────────────┘ ┆ │ │ │ │ ┆
┆ │ String Integer ┆ │ ┆ Element Value xs:string ┆
┆ │ Date Float ┆ │ ┆ Attribute Type xs:date ┆
┆ │ Time Boolean ┆ ├──────────►┆ xs:boolean ┆
┆ │ ┆ │ ┆ xs:anyURI ┆
┆ └──────┐ ┆ │ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
┆ │ ┆ │
┆ Contains ┆ │ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆ more Models ┆ │ ┆ JSON Model ┆
┆ (recursive) ┆ │ ┆ │ ┆
┆ ┆ │ ┆ ┌──────┴──────┐ ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯ └──────────►┆ │ │ ┆
┆ Models Value Types ┆
┆ │ │ ┆
┆ │ │ ┆
┆ ┌────┴───┐ ┌───┴──┐ ┆
┆ │ │ │ │ ┆
┆ object array number string ┆
┆ value boolean null ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
.Model transformation of a LutaML Model to another LutaML Model
[source]
╔═══════════════════════╗ ╔══════════════════╗ ╔═══════════════════════╗
║LutaML Model Class FOO ║ ║LutaML Transformer║ ║LutaML Model Class BAR ║
╚═══════════════════════╝ ╚══════════════════╝ ╚═══════════════════════╝
╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆ Model ┆ ┆ Model ┆
┆ │ ┆ ┌────────────────┐ ┆ │ ┆
┆ ┌────────┴──┐ ┆ │ │ ┆ ┌────────┴──┐ ┆
┆ │ │ ┆ │ Model │ ┆ │ │ ┆
┆ Models Value Types ┆───►│ Transformation │───►┆ Models Value Types ┆
┆ │ │ ┆◄───│ & │◄───┆ │ │ ┆
┆ │ │ ┆ │ Mapping Rules │ ┆ │ │ ┆
┆ │ ┌──────┴──┐ ┆ │ │ ┆ │ ┌──────┴──┐ ┆
┆ │ │ │ ┆ └────────────────┘ ┆ │ │ │ ┆
┆ │ String Integer ┆ ┆ │ String Integer ┆
┆ │ Date Float ┆ ┆ │ Date Float ┆
┆ │ Time Boolean ┆ ┆ │ Time Boolean ┆
┆ │ ┆ ┆ │ ┆
┆ └──────┐ ┆ ┆ └──────┐ ┆
┆ │ ┆ ┆ │ ┆
┆ Contains ┆ ┆ Contains ┆
┆ more Models ┆ ┆ more Models ┆
┆ (recursive) ┆ ┆ (recursive) ┆
┆ ┆ ┆ ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
.The Value
class, transformation, and serialization formats
[source]
╔═══════════════════════╗ ╔═══════════════════════╗
║LutaML Value Class FOO ║ ║ Serialization Value ║
╚═══════════════════════╝ ╚═══════════════════════╝
╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆ ┌───────────────┐ ┆ ┆ ┌───────────────┐ ┆
┆ │ Value │ ┆ ┌──────────────────┐ ┆ │ XML Value │ ┆
┆ └───────────────┘ ┆──►│ Value Serializer │──►┆ └───────────────┘ ┆
┆ ┌───────────────┐ ┆ └──────────────────┘ ┆ ┌───────────────┐ ┆
┆ │Primitive Types│ ┆ ┆ │XML Value Types│ ┆
┆ └───────────────┘ ┆ ┆ └───────────────┘ ┆
┆ ┌───┘ ┆ ┆ ┌───┘ ┆
┆ ├─ string ┆ ┆ ├─ xs:string ┆
┆ ├─ integer ┆ ┆ ├─ xs:integer ┆
┆ ├─ float ┆ ┆ ├─ xs:decimal ┆
┆ ├─ boolean ┆ ┆ ├─ xs:boolean ┆
┆ ├─ date ┆ ┆ ├─ xs:date ┆
┆ ├─ time_without_date ┆ ┆ ├─ xs:time ┆
┆ ├─ date_time ┆ ┆ ├─ xs:dateTime ┆
┆ ├─ time ┆ ┆ ├─ xs:decimal ┆
┆ ├─ decimal ┆ ┆ ├─ xs:anyType ┆
┆ └─ hash ┆ ┆ └─ (complex element) ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
│
▼
┌───────────────────┐
│ Value Transformer │
└───────────────────┘
│
▼
╔═══════════════════════╗
║LutaML Value Class BAR ║
╚═══════════════════════╝
╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆ ┌───────────────┐ ┆
┆ │ Value │ ┆
┆ └───────────────┘ ┆
┆ ┌───────────────┐ ┆
┆ │Primitive Types│ ┆
┆ └───────────────┘ ┆
┆ ┌───┘ ┆
┆ ├─ string ┆
┆ ├─ integer ┆
┆ ├─ float ┆
┆ ├─ boolean ┆
┆ ├─ date ┆
┆ ├─ time_without_date ┆
┆ ├─ date_time ┆
┆ ├─ time ┆
┆ ├─ decimal ┆
┆ └─ hash ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
.Example of LutaML Model instance transformed into a serialization model and serialized to JSON
[source]
╔═════════════════════╗ ╔═════════════════════╗ ╔═════════════════════╗
║ Studio (Core Model) ║ ║ JSON Model ║ ║ Serialized JSON ║
╚═════════════════════╝ ╚═════════════════════╝ ╚═════════════════════╝
name: "Studio 1" ┌─► { ┌─► {
address: │ "name": "...", │ "name": "Studio 1",
├── street: "..." │ "address": { │ "address": {
└── city: "..." │ "street": "...", │ "street": "...",
kilns: ──┤ "city": "..." ──┤ "city": "..."
├── count: 3 │ }, │ },
└── temp: 1200 │ "kilnsCount": ..., │ "kilnsCount": 3,
│ "kilnsTemp": ... │ "kilnsTemp": 1200
└─► } └─► }
====
== Installation
Add this line to your application's Gemfile:
[source,ruby]
gem 'lutaml-model'
And then execute:
[source,shell]
bundle install
Or install it yourself as:
[source,shell]
gem install lutaml-model
== Model
=== General
There are two ways to define an information model in Lutaml::Model:
- Inheriting from the
Lutaml::Model::Serializable
class - Including the
Lutaml::Model::Serialize
module
[[define-through-inheritance]]
=== Definition 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.
[source,ruby]
require 'lutaml/model'
class Kiln < Lutaml::Model::Serializable
attribute :brand, :string
attribute :capacity, :integer
attribute :temperature, :integer
end
[[define-through-inclusion]]
=== Definition 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.
[source,ruby]
require 'lutaml/model'
class Kiln < SomeSuperClass
include Lutaml::Model::Serialize
attribute :brand, :string
attribute :capacity, :integer
attribute :temperature, :integer
end
=== 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.
[source,ruby]
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 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:
[source,ruby]
require 'bigdecimal'
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.
.Using a custom value type to normalize a postcode with minimal methods
[example]
[source,ruby]
class FiveDigitPostCode < Lutaml::Model::Type::String
def self.cast(value)
value = value.to_s if value.is_a?(Integer)
unless value.is_a?(::String)
raise Lutaml::Model::InvalidValueError, "Invalid value for type 'FiveDigitPostCode'"
end
# Pad zeros to the left
value.rjust(5, '0')
end
def self.serialize(value)
value
end
end
class Studio < Lutaml::Model::Serializable
attribute :postcode, FiveDigitPostCode
end
====
=== 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. json
, xml
, yaml
, toml
).
.Using custom serialization methods to handle a high-precision date-time type
[example]
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
.
[source,ruby]
class HighPrecisionDateTime < Lutaml::Model::Type::DateTime
Inherit the self.cast(value)
and self.serialize(value)
methods
from Lutaml::Model::Type::DateTime
The format looks like this 2012-04-07T01:51:37.112+02:00
def self.from_xml(xml_string)
::DateTime.parse(xml_string)
end
The %L adds milliseconds to the time
def to_xml
value.strftime('%Y-%m-%dT%H:%M:%S.%L%:z')
end
end
class Ceramic < Lutaml::Model::Serializable
attribute :kiln_firing_time, HighPrecisionDateTime
xml do
root 'ceramic'
map_element 'kilnFiringTime', to: :kiln_firing_time
# ...
end
end
An XML snippet with the high-precision date-time type:
[source,xml]
2012-04-07T01:51:37.112+02:00
----
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.
[source,ruby]
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"}
====
== 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:
- Value (inherits from Lutaml::Model::Value)
- Model (inherits from Lutaml::Model::Serializable)
Syntax:
[source,ruby]
attribute :name_of_attribute, Type
Where,
name_of_attribute
:: The defined name of the attribute.
Type
:: The type of the attribute.
.Using the attribute
class method to define simple attributes
[example]
[source,ruby]
class Studio < Lutaml::Model::Serializable
attribute :name, :string
attribute :address, :string
attribute :established, :date
end
[source,ruby]
s = Studio.new(name: 'Pottery Studio', address: '123 Clay St', established: Date.new(2020, 1, 1))
puts s.name
#=> "Pottery Studio"
puts s.address
#=> "123 Clay St"
puts s.established
#=> <Date: 2020-01-01>
====
An attribute with a defined value type also accepts values that are of a class that
is a subclass of the defined type.
This means that the assigned Type
accepts polymorphic classes as long as the
assigned instance is of a class that either inherits from the declared type or
matches it.
.Using a superclass type receive child object
[example]
[source,ruby]
class Studio < Lutaml::Model::Serializable
attribute :name, :string
end
class CeramicStudio < Studio
attribute :clay_type, :string
end
class PotteryClass < Lutaml::Model::Serializable
attribute :studio, Studio
end
[source,ruby]
This works
s = Studio.new(name: 'Pottery Studio')
p = PotteryClass.new(studio: s)
p.studio
=> <Studio:0x0000000104ac7240 @name="Pottery Studio", @address=nil, @established=nil>
A subclass of Studio is also valid
s = CeramicStudio.new(name: 'Ceramic World', clay_type: 'Red')
p = PotteryClass.new(studio: s)
p.studio
=> <CeramicStudio:0x0000000104ac7240 @name="Ceramic World", @address=nil, @established=nil, @clay_type="Red">
p.studio.name
=> "Ceramic World"
p.studio.clay_type
=> "Red"
====
=== Collection attributes
Define attributes as collections (arrays or hashes) to store multiple values
using the collection
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]
When set to 0..1
, it means that the attribute is optional, it could be empty
or contain one object of the declared class.
[example]
When set to 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.
[example]
When set to 5..10means 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:
[source,ruby]
attribute :name_of_attribute, Type, collection: true
attribute :name_of_attribute, Type, collection: {min}..{max}
attribute :name_of_attribute, Type, collection: {min}..
.Using the collection
option to define a collection attribute
[example]
[source,ruby]
class Studio < Lutaml::Model::Serializable
attribute :location, :string
attribute :potters, :string, collection: true
attribute :address, :string, collection: 1..2
attribute :hobbies, :string, collection: 0..
end
[source,ruby]
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']
====
=== Derived attributes
A derived attribute is computed dynamically based on an instance method instead of storing a static value. It is defined using the method:
option.
Syntax:
[source,ruby]
attribute :name_of_attribute, method: :instance_method_name
.Defining methods as attributes
[example]
[source,ruby]
class Invoice < Lutaml::Model::Serializable
attribute :subtotal, :float
attribute :tax, :float
attribute :total, method: :total_value
def total_value
subtotal + tax
end
end
i = Invoice.new(subtotal: 100.0, tax: 12.0)
i.total
#=> 112.0
puts i.to_yaml
#=> ---
#=> subtotal: 100.0
#=> tax: 12.0
#=> total: 112.0
====
=== 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:
[source,ruby]
choice(min: {min}, max: {max}) do
{block}
end
Where,
min
:: The minimum number of elements that must be included.
max
:: The maximum number of elements that can be included.
block
:: The block of elements that must be included. The block can contain
multiple attribute
and choice
directives.
.Using the choice
directive to define a set of attributes with a range
[example]
[source,ruby]
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
end
end
This means that the Studio
class must have at least one and at most three
attributes.
- The first choice must have at least one and at most two attributes.
- The second attribute is the
completeName
. - The first choice can have either the
prefix
and forename
attributes or just the forename
attribute. - The last attribute
completeName
is optional.
====
=== Importable models for reuse
An importable model is a model that can be imported into another model using the
import_*
directive.
Such a model is specified by setting the model's XML serialization configuration
with the no_root
directive.
As a result, 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: This feature only works with XML for now. The import order determines how
elements and attributes are overwritten.
Models with no_root
can only be parsed through parent models.
Direct calling NoRootModel.from_xml
will raise a NoRootMappingError
.
Namespaces are not supported in importable models. If namespace
is defined with
no_root
, NoRootNamespaceError
will raise.
[example]
[source,ruby]
class GroupOfItems < Lutaml::Model::Serializable
attribute :name, :string
attribute :type, :string
attribute :code, :string
xml do
no_root
sequence do
map_element "name", to: :name
map_element "type", to: :type, namespace: "http://www.example.com", prefix: "ex1"
end
map_attribute "code", to: :code
end
end
class ComplexType < Lutaml::Model::Serializable
attribute :tag, AttributeValueType
attribute :content, :string
attribute :group, :string
import_model_attributes GroupOfItems
xml do
root "GroupOfItems"
map_attribute "tag", to: :tag
map_content to: :content
map_element :group, to: :group
import_model_mappings GroupOfItems
end
end
class SimpleType < Lutaml::Model::Serializable
import_model GroupOfItems
end
class GenericType < Lutaml::Model::Serializable
import_model_mappings GroupOfItems
end
[source,xml]
Name
Type
----
[source,ruby]
parsed = GroupOfItems.from_xml(xml)
Lutaml::Model::NoRootMappingError: "GroupOfItems has no_root
, it allowed only for reusable models"
====
=== 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:
[source,ruby]
attribute :name_of_attribute, Type, values: [value1, value2, ...]
The values set inside the values:
option can be of any type, but they must
match the type of the attribute. The values are compared using the ==
operator,
so the type must implement the ==
method.
Also, If all the elements in values
directive are strings then lutaml-model
add some enum convenience methods, for each of the value the following three methods are added
value1
: will return value if 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.
.Using the values
directive to define acceptable values for an attribute (basic types)
[example]
[source,ruby]
class GlazeTechnique < Lutaml::Model::Serializable
attribute :name, :string, values: ["Celadon", "Raku", "Majolica"]
end
[source,ruby]
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.
.Using the values
directive to define acceptable values for an attribute (Serializable objects)
[example]
[source,ruby]
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :firing_temperature, :integer
end
class CeramicCollection < Lutaml::Model::Serializable
attribute :featured_piece,
Ceramic,
values: [
Ceramic.new(type: "Porcelain", firing_temperature: 1300),
Ceramic.new(type: "Stoneware", firing_temperature: 1200),
Ceramic.new(type: "Earthenware", firing_temperature: 1000),
]
end
[source,ruby]
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.
.Using the validate
method to check if all attributes have valid values
[example]
[source,ruby]
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:
[source,ruby]
attribute :name_of_attribute, :string, pattern: /regex/
.Using the 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})$/
.
[source,ruby]
class Glaze < Lutaml::Model::Serializable
attribute :color, :string, pattern: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/
end
[source,ruby]
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:
[source,ruby]
attribute :name_of_attribute, Type, default: -> { value }
.Using the default
option to set a default value for an attribute
[example]
[source,ruby]
class Glaze < Lutaml::Model::Serializable
attribute :color, :string, default: -> { 'Clear' }
attribute :temperature, :integer, default: -> { 1050 }
end
[source,ruby]
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:
[source,ruby]
attribute :name_of_attribute, :string, raw: true
.Using the raw
option to read raw value for an XML attribute
[example]
[source,ruby]
class Person < Lutaml::Model::Serializable
attribute :name, :string
attribute :description, :string, raw: true
end
For the following XML snippet:
[source,xml]
John Doe
A
fictional person commonly used as a
placeholder name.
----
[source,ruby]
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>
====
== Serialization model mappings
=== General
Lutaml::Model allows you to translate a data model into serialization models of
various serialization formats including XML, JSON, YAML, and TOML.
Depending on the serialization format, different methods are supported for
defining serialization and deserialization mappings.
Serialization model mappings are defined under the xml
, json
, yaml
, and
toml
blocks.
.Using the xml
, json
, yaml
, toml
and key_value
blocks to define serialization mappings
[source,ruby]
class Example < Lutaml::Model::Serializable
xml do
# ...
end
json do
# ...
end
yaml do
# ...
end
toml do
# ...
end
key_value do
# ...
end
end
=== 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:
[source,ruby]
xml do
root 'xml_element_name'
end
.Setting the root element name to example
[example]
[source,ruby]
class Example < Lutaml::Model::Serializable
xml do
root 'example'
end
end
[source,ruby]
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:
[source,ruby]
xml do
no_root
end
[example]
[source,ruby]
class NameAndCode < Lutaml::Model::Serializable
attribute :name, :string
attribute :code, :string
xml do
no_root
map_element "code", to: :code
map_element "name", to: :name
end
end
[source,xml]
Name
ID-001
[source,ruby]
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:
- nested tags
- attributes
- text nodes
The map_all
tag is exclusive and cannot be combined with other mappings
(map_element
, map_content
) except for map_attribute
for the same element,
ensuring it captures the entire inner XML content.
NOTE: An error is raised if map_all
is defined alongside any other mapping in
the same XML mapping context.
Syntax:
[source,ruby]
xml do
map_all to: :name_of_attribute
end
.Mapping all the content using map_all
[example]
[source,ruby]
class ExampleMapping < Lutaml::Model::Serializable
attribute :description, :string
xml do
map_all to: :description
end
end
[source,xml]
Content with tags and formatting.
[source,ruby]
parsed = ExampleMapping.from_xml(xml)
puts parsed.all_content
"Content with tags and formatting."
====
==== 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:
[source,ruby]
xml do
map_element 'xml_element_name', to: :name_of_attribute
end
.Mapping the name
tag to the name
attribute
[example]
[source,ruby]
class Example < Lutaml::Model::Serializable
attribute :name, :string
xml do
root 'example'
map_element 'name', to: :name
end
end
[source,xml]
John Doe
[source,ruby]
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.
.The mapped tag name is used as the root name
[example]
[source,ruby]
class RecordDate < Lutaml::Model::Serializable
attribute :content, :string
xml do
root "recordDate"
map_content to: :content
end
end
class OriginInfo < Lutaml::Model::Serializable
attribute :date_issued, RecordDate, collection: true
xml do
root "originInfo"
map_element "dateIssued", to: :date_issued
end
end
[source,ruby]
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:
[source,ruby]
xml do
map_attribute 'xml_attribute_name', to: :name_of_attribute
end
.Using map_attribute
to map the value
attribute
[example]
The following class will parse the XML snippet below:
[source,ruby]
class Example < Lutaml::Model::Serializable
attribute :value, :integer
xml do
root 'example'
map_attribute 'value', to: :value
end
end
[source,xml]
John Doe
[source,ruby]
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.
[example]
The following class will parse the XML snippet below:
[source,ruby]
class Attribute < Lutaml::Model::Serializable
attribute :value, :integer
xml do
root 'example'
map_attribute 'value', to: :value, namespace: "http://www.tech.co/XMI", prefix: "xl"
end
end
[source,xml]
[source,ruby]
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:
[source,ruby]
xml do
map_content to: :name_of_attribute
end
.Using map_content
to map content of the description
tag
[example]
The following class will parse the XML snippet below:
[source,ruby]
class Example < Lutaml::Model::Serializable
attribute :description, :string
xml do
root 'example'
map_content to: :description
end
end
[source,xml]
John Doe is my moniker.
[source,ruby]
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:
[source,ruby]
xml do
map_content to: :name_of_attribute, cdata: (true | false)
map_element :name, to: :name, cdata: (true | false)
end
.Using cdata
to map CDATA content
[example]
The following class will parse the XML snippet below:
[source,ruby]
class Example < Lutaml::Model::Serializable
attribute :name, :string
attribute :description, :string
attribute :title, :string
attribute :note, :string
xml do
root 'example'
map_element :name, to: :name, cdata: true
map_content to: :description, cdata: true
map_element :title, to: :title, cdata: false
map_element :note, to: :note, cdata: false
end
end
[source,xml]
<![CDATA[Lutaml]]>Careful
[source,ruby]
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
[example]
The following class will parse the XML snippet below:
[source,ruby]
class Ceramic < Lutaml::Model::Serializable
attribute :name, :string
attribute :description, :string
attribute :temperature, :integer
xml do
root 'ceramic'
map_element 'name', to: :name
map_attribute 'temperature', to: :temperature
map_content to: :description
end
end
[source,xml]
Porcelain Vase with celadon glaze.
[source,ruby]
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:
.Setting default namespace at the root element
[source,ruby]
.Setting a prefixed namespace at the root element
[source,ruby]
.Using the namespace
method to set the namespace for the root element
[example]
[source,ruby]
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, :string
xml do
root 'Ceramic'
namespace 'http://example.com/ceramic'
map_element 'Type', to: :type
map_element 'Glaze', to: :glaze
end
end
[source,xml]
PorcelainClear
[source,ruby]
Ceramic.from_xml(xml_file)
#<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear">
Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
#PorcelainClear
====
.Using the namespace
method to set a prefixed namespace for the root element
[example]
[source,ruby]
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, :string
xml do
root 'Ceramic'
namespace 'http://example.com/ceramic', 'cer'
map_element 'Type', to: :type
map_element 'Glaze', to: :glaze
end
end
[source,xml]
[source,ruby]
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:
[source,ruby]
xml do
map_element 'xml_element_name', to: :name_of_attribute,
namespace: 'http://example.com/namespace',
prefix: 'prefix'
end
namespace
:: The XML namespace used by this element
prefix
:: The XML namespace prefix used by this element (optional)
.Using the 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.
[source,ruby]
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, Glaze
xml do
root 'Ceramic'
namespace 'http://example.com/ceramic'
map_element 'Type', to: :type
map_element 'Glaze', to: :glaze, namespace: 'http://example.com/glaze', prefix: "glz"
end
end
class Glaze < Lutaml::Model::Serializable
attribute :color, :string
attribute :temperature, :integer
xml do
root 'Glaze'
namespace 'http://example.com/old_glaze', 'glaze'
map_element 'color', to: :color
map_element 'temperature', to: :temperature
end
end
[source,xml]
Porcelain
Clear
1050
----
[source,ruby]
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:
[source,ruby]
xml do
map_element 'xml_element_name', to: :name_of_attribute, namespace: :inherit
end
.Using the inherit
option to inherit the namespace from the root element
[example]
In this example, the Type
element will inherit the namespace from the root.
[source,ruby]
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
[source,xml]
[source,ruby]
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"
clr:color="navy-blue">
cera:TypePorcelain</cera:Type>
Clear
</cera: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.
[example]
[source,xml]
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:
- when defining the model;
- when referencing the model.
NOTE: This feature is not supported by Shale.
To specify mixed content, the mixed: true
option needs to be set at the
xml
block's root
method.
Syntax:
[source,ruby]
xml do
root 'xml_element_name', mixed: true
end
.Applying mixed
to treat root as mixed content
[example]
[source,ruby]
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
end
end
[source,ruby]
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:
[source,ruby]
xml do
root 'xml_element_name', ordered: true
end
.Applying ordered
to treat root as ordered content
[example]
[source,ruby]
class RootOrderedContent < Lutaml::Model::Serializable
attribute :bold, :string
attribute :italic, :string
attribute :underline, :string
xml do
root "RootOrderedContent", ordered: true
map_element :bold, to: :bold
map_element :italic, to: :italic
map_element :underline, to: :underline
end
end
[source,xml]
Moon
384,400 km
bell
----
[source,ruby]
instance = RootOrderedContent.from_xml(xml)
#<RootOrderedContent:0x0000000104ac7240 @bold="bell", @italic="384,400 km", @underline="Moon">
instance.to_xml
#Moon384,400 kmbell
Without Ordered True:
[source,ruby]
class RootOrderedContent < Lutaml::Model::Serializable
attribute :bold, :string
attribute :italic, :string
attribute :underline, :string
xml do
root "RootOrderedContent"
map_element :bold, to: :bold
map_element :italic, to: :italic
map_element :underline, to: :underline
end
end
[source,xml]
Moon
384,400 km
bell
----
[source,ruby]
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:
[source,ruby]
xml do
sequence do
map_element 'xml_element_name_1', to: :name_of_attribute_1
map_element 'xml_element_name_2', to: :name_of_attribute_2
# Add more map_element lines as needed to establish a complete sequence
end
end
The appearance of the elements in the XML document must match the order defined
in the sequence
block. In this case, the <xml_element_name_1>
element
should appear before the <xml_element_name_2>
element.
.Using the sequence
keyword to define a set of elements in desired order.
[example]
[source,ruby]
class Kiln < Lutaml::Model::Serializable
attribute :id, :string
attribute :name, :string
attribute :type, :string
attribute :color, :string
xml do
sequence do
map_element :id, to: :id
map_element :name, to: :name
map_element :type, to: :type
map_element :color, to: :color
end
end
end
class KilnCollection < Lutaml::Model::Serializable
attribute :kiln, Kiln, collection: 1..2
xml do
root "collection"
map_element "kiln", to: :kiln
end
end
[source,xml]
1
Nick
Hard
Black
2
John
Soft
White
----
[source,ruby]
parsed = Kiln.from_xml(xml)
=> [
#<Kiln:0x0000000104ac7240 @id="1", @name="Nick", @type="Hard", @color="Black">,
#<Kiln:0x0000000104ac7240 @id="2", @name="John", @type="Soft", @color="White">
#]
bad_xml = <<~HERE
Nick
1
Black
Hard
HERE
> parsed = Kiln.from_xml(bad_xml)
# => Lutaml::Model::ValidationError: Element 'name' is out of order in 'kiln' element
----
====
[[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:
[source,xml]
LutaML::Model supports the xsi:schemaLocation
attribute in all XML
serializations by default, through the schema_location
attribute on the model
instance object.
.Retrieving and setting the 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.
[source,ruby]
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
[source,ruby]
c = Ceramic.from_xml(xml_content)
=>
#<Ceramic:0x00000001222bdd60
...
schema_loc = c.schema_location
#<Lutaml::Model::SchemaLocation:0x0000000122773760
...
schema_loc
=>
#<Lutaml::Model::SchemaLocation:0x0000000122773760
@namespace="http://www.w3.org/2001/XMLSchema-instance",
@original_schema_location="http://example.com/ceramic http://example.com/ceramic.xsd http://example.com/color http://example.com/color.xsd",
@prefix="xsi",
@schema_location=
[#<Lutaml::Model::Location:0x00000001222bd018 @location="http://example.com/ceramic.xsd", @namespace="http://example.com/ceramic">,
#<Lutaml::Model::Location:0x00000001222bcfc8 @location="http://example.com/color.xsd", @namespace="http://example.com/color">]>
new_c = Ceramic.new(type: "Porcelain", glaze: "Clear", color: "navy-blue", schema_location: schema_loc).to_xml
puts new_c
<cera:Ceramic
clr:color="navy-blue"
xsi:schemaLocation="
">
cera:TypePorcelain</cera:Type>
</cera:Ceramic>
====
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:
[source,ruby]
ModelClassInstance.encoding = {encoding_value}
Where,
ModelClassInstance
:: An instance of the class that inherits from
Lutaml::Model::Serializable.
{encoding_value}
:: The encoding of the output data.
.Character encoding set to instance is reflected in its serialization output
[example]
[source,ruby]
class JapaneseCeramic < Lutaml::Model::Serializable
attribute :glaze_type, :string
attribute :description, :string
xml do
root 'JapaneseCeramic'
map_attribute 'glazeType', to: :glaze_type
map_element 'description' to: :description
end
end
[source,ruby]
Create a new instance with UTF-8 data
instance = JapaneseCeramic.new(glaze_type: "志野釉", description: "東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)")
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
Set character encoding to Shift_JIS
instance.encoding = "Shift_JIS"
#=> "Shift_JIS"
Serialize the instance
serialization_output = instance.to_xml
#=> #\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}
Check character encoding of output
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:
[source,ruby]
ModelClassInstance.to_xml(encoding: {encoding_value})
Where,
ModelClassInstance
:: An instance of the class that inherits from
Lutaml::Model::Serializable.
{encoding_value}
:: The encoding of the output data.
[example]
The following class will parse the XML snippet below:
[source,ruby]
class Ceramic < Lutaml::Model::Serializable
attribute :potter, :string
attribute :description, :string
attribute :temperature, :integer
xml do
root 'ceramic'
map_element 'potter', to: :potter
map_content to: :description
end
end
[source,xml]
John & Jane A ∑ series of ∏ porcelain µ vases.
[source,ruby]
Object with attributes
ceramic_instance = Ceramic.new(potter: "John & Jane", description: " A ∑ series of ∏ porcelain µ vases.")
#<Ceramic:0x0000000104ac7240 @potter="John & Jane", @description=" A ∑ series of ∏ porcelain µ vases.">
Parsing the XML snippet with the default encoding of UTF-8
ceramic_parsed = Ceramic.from_xml(xml)
#<Ceramic:0x0000000104ac7242 @potter="John & Jane", @description=" A ∑ series of ∏ porcelain µ vases.">
Object with attributes is equal to the parsed object
ceramic_parsed == ceramic_instance
true
Using the default encoding of UTF-8
ceramic_instance.to_xml
#John & Jane A ∑ series of ∏ porcelain µ vases.
Using the default encoding of the adapter, which is UTF-8 in this case
ceramic_instance.to_xml(encoding: nil)
#John & Jane A ∑ series of ∏ porcelain µ vases.
Using ASCII encoding
ceramic_instance.to_xml(encoding: "ASCII")
#John & Jane A ∑ series of ∏ porcelain µ vases.
====
.Character encoding set at to_xml
overrides instance encoding
[example]
[source,ruby]
class JapaneseCeramic < Lutaml::Model::Serializable
attribute :glaze_type, :string
attribute :description, :string
xml do
root 'JapaneseCeramic'
map_attribute 'glazeType', to: :glaze_type
map_element 'description' to: :description
end
end
[source,ruby]
Create a new instance with UTF-8 data
instance = JapaneseCeramic.new(glaze_type: "志野釉", description: "東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)")
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
Set character encoding to Shift_JIS
instance.encoding = "Shift_JIS"
#=> "Shift_JIS"
Serialize the instance
serialization_output = instance.to_xml(encoding: "UTF-8")
#=> #志野釉東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)
Check character encoding of output
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:
[source,ruby]
ModelClass.from_{format}(string_in_format, encoding: {encoding_value})
Where,
ModelClass
:: The class that inherits from Lutaml::Model::Serializable.
{format}
:: The format of the input data, e.g. xml
, json
, yaml
, toml
.
string_in_format
:: The input data in the specified format.
{encoding_value}
:: The encoding of the input data.
.Setting the 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.
[source,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}
----
[source,ruby]
Parse the XML snippet with the encoding of Shift_JIS
instance = JapaneseCeramic.from_xml(xml, encoding: "Shift_JIS")
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
Check character encoding of the instance
instance.encoding
#=> "Shift_JIS"
Serialize the instance using UTF-8
serialization_output = instance.to_xml(encoding: "UTF-8")
#=> #志野釉東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)
serialization_output.encoding
#=> "UTF-8"
====
.When the 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.
[source,xml]
志野釉
東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)
----
In adapters that use a default encoding of UTF-8
, the content is parsed
properly.
[source,ruby]
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.
[source,ruby]
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 an invalid encoding to deserialize causes data corruption
[example]
Using the definition of JapaneseCeramic
at <>.
This XML snippet is in UTF-8.
[source,xml]
志野釉
東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)
----
[source,ruby]
JapaneseCeramic.from_xml(xml, encoding: "Shift_JIS")
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="�菑���p���P", @description="�東京国立博物館コレクションの篠茶碗�橋本�桃山時代�">
====
=== Key value data models
==== General
Key-value data models like JSON, YAML, and TOML all share a similar structure
where data is stored as key-value pairs.
Lutaml::Model
works with these formats in a similar way.
==== Mapping
The map
method is used to define key-value mappings.
Syntax:
[source,ruby]
json | yaml | toml | key_value do
map 'key_value_model_attribute_name', to: :name_of_attribute
end
==== Unified mapping
The key_value
method is a streamlined way to map all attributes for
serialization into key-value formats including 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.
.Using the 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.
[source,ruby]
class CeramicModel < Lutaml::Model::Serializable
attribute :color, :string
attribute :glaze, :string
attribute :description, :string
key_value do
map :color, to: color
map :glz, to: :glaze
map :desc, to: :description
end
Equivalent to the JSON, YAML, and TOML mappings.
json and yaml and toml do
map :id, to: color
map :name, to: :full_name
map :status, to: :current_status
end
end
[source,json]
{
"color": "Navy Blue",
"glz": "Clear",
"desc": "A ceramic with a navy blue color and clear glaze."
}
[source,yaml]
color: Navy Blue
glz: Clear
desc: A ceramic with a navy blue color and clear glaze.
[source,ruby]
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, including:
json
for the JSON formatyaml
for the YAML formattoml
for the TOML format
.Using the map
method to define key-value mappings per format
[example]
[source,ruby]
class Example < Lutaml::Model::Serializable
attribute :name, :string
attribute :value, :integer
json do
map 'name', to: :name
map 'value', to: :value
end
yaml do
map 'name', to: :name
map 'value', to: :value
end
toml do
map 'name', to: :name
map 'value', to: :value
end
end
[source,json]
{
"name": "John Doe",
"value": 28
}
[source,ruby]
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:
[source,ruby]
json | yaml | toml | key_value do
map_all to: :name_of_attribute
end
.Using map_all
to capture all content across different formats
[example]
[source,ruby]
class Document < Lutaml::Model::Serializable
attribute :content, :string
json do
map_all to: :content
end
yaml do
map_all to: :content
end
toml do
map_all to: :content
end
end
For JSON:
[source,json]
{
"sections": [
{ "title": "Introduction", "text": "Chapter 1" },
{ "title": "Conclusion", "text": "Final chapter" }
],
"metadata": {
"author": "John Doe",
"date": "2024-01-15"
}
}
For YAML:
[source,yaml]
sections:
- title: Introduction
text: Chapter 1
- title: Conclusion
text: Final chapter
metadata:
author: John Doe
date: 2024-01-15
The content is preserved exactly as provided:
[source,ruby]
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.
[example]
[source,ruby]
class Glaze < Lutaml::Model::Serializable
attribute :color, :string
attribute :temperature, :integer
json do
map 'color', to: :color
map 'temperature', to: :temperature
end
end
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, Glaze
json do
map 'type', to: :type
map 'glaze', to: :glaze
end
end
[source,json]
{
"type": "Porcelain",
"glaze": {
"color": "Clear",
"temperature": 1050
}
}
[source,ruby]
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:
[[collection-keyed-by-value]]
.A YAML collection as a keyed object, each key with value of the id
attribute
[source,yaml]
vase1:
name: Imperial Vase
bowl2:
name: 18th Century Bowl
[[collection-unkeyed-by-value]]
.A YAML collection as an array, the id
attribute value located inside each element
[source,yaml]
- id: vase1
name: Imperial Vase
- id: bowl2
name: 18th Century Bowl
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:
[source,ruby]
class SomeKeyedCollection < Lutaml::Model::Serializable
attribute :name_of_attribute, AttributeValueType, collection: true
json | yaml | toml | key_value do
map to: :name_of_attribute, <1>
root_mappings: { <2>
# :key
is a reserved keyword
value_type_attribute_name_for_key: :key, <3>
# :value
is a reserved keyword (and optional)
value_type_attribute_name_for_value: :value, <4>
# [path name]
represents the path to access the value in the
# serialization data model to be assigned to
# AttributeValueType.value_type_attribute_name_for_custom_type
value_type_attribute_name_for_custom_type: [path name] <5>
}
end
end
class AttributeValueType < Lutaml::Model::Serializable
attribute :value_type_attribute_name_for_key, :string
attribute :value_type_attribute_name_for_value, :string
attribute :value_type_attribute_name_for_custom_type, CustomType
end
<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 "key" of the keyed collection maps to a particular attribute of the
collection's instance object.
- The "value" of the keyed collection (with its various content) maps to the
collection's instance object following the collection's instance object type's
default mappings.
The root_mappings
option should only contain one mapping, and the mapping
must lead to the :key
keyword.
Syntax:
[source,ruby]
class SomeKeyedCollection < Lutaml::Model::Serializable
attribute :name_of_attribute, AttributeValueType, collection: true
json | yaml | toml | key_value do
map to: :name_of_attribute,
root_mappings: {
value_type_attribute_name_for_key: :key, <1>
}
end
end
class AttributeValueType < Lutaml::Model::Serializable
attribute :value_type_attribute_name_for_key, :string
attribute :value_type_attribute_name_for_value, :string
attribute :value_type_attribute_name_for_custom_type, CustomType
end
<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
).
.Using map
with root_mappings
(only key
) to map a keyed collection into individual models
[example]
Given this data:
[source,yaml]
vase1:
name: Imperial Vase
bowl2:
name: 18th Century Bowl
A model can be defined for this YAML as follows:
[source,ruby]
This is a normal Lutaml::Model class
class Ceramic < Lutaml::Model::Serializable
attribute :ceramic_id, :string
attribute :ceramic_name, :string
key_value do
map 'id', to: :ceramic_id
map 'name', to: :ceramic_name
end
end
This is Lutaml::Model class that represents the collection of Ceramic objects
class CeramicCollection < Lutaml::Model::Serializable
attribute :ceramics, Ceramic, collection: true
key_value do
map to: :ceramics, # All data goes to the ceramics
attribute
root_mappings: {
# The key of an object in this collection is mapped to the ceramic_id
# attribute of the Ceramic object.
ceramic_id: :key # "key" is a reserved keyword
}
end
end
[source,ruby]
Parsing the YAML collection with dynamic data keys
ceramic_collection = CeramicCollection.from_yaml(yaml)
#<CeramicCollection:0x0000000104ac7240
@ceramics=
[#<Ceramic:0x0000000104ac6e30 @ceramic_id="vase1", @ceramic_name="Imperial Vase">,
#<Ceramic:0x0000000104ac58f0 @ceramic_id="bowl2", @ceramic_name="18th Century Bowl">]
NOTE: When an individual Ceramic object is serialized, the id
attribute is
the original key in the incoming YAML data, and because there were no mappings defined along with the :key
, everyting is mapped to the Ceramic
object using the mappings defined in the Ceramic
class.
first_ceramic = ceramic_collection.ceramics.first
puts first_ceramic.to_yaml
=>
---
id: vase1
name: Imperial Vase
NOTE: When in a collection, the ceramic_id
attribute is used to key the data,
and it disappears from the individual object.
puts ceramic_collection.to_yaml
=>
---
vase1:
name: Imperial Vase
bowl2:
name: 18th Century Bowl
NOTE: When the collection is serialized, the ceramic_id
attribute is used to
key the data. This is defined through the map
with root_mappings
method in
CeramicCollection.
new_collection = CeramicCollection.new(ceramics: [
Ceramic.new(ceramic_id: "vase1", ceramic_name: "Imperial Vase"),
Ceramic.new(ceramic_id: "bowl2", ceramic_name: "18th Century Bowl")
])
puts new_collection.to_yaml
=>
---
vase1:
name: Imperial Vase
bowl2:
name: 18th Century Bowl
====
===== 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:
[source,ruby]
class SomeKeyedCollection < Lutaml::Model::Serializable
attribute :name_of_attribute, AttributeValueType, collection: true
json | yaml | toml | key_value do
map to: :name_of_attribute,
root_mappings: {
value_type_attribute_name_for_key: :key, <1>
value_type_attribute_name_for_value_data_1: "serialization_format_name_1", <2>
value_type_attribute_name_for_value_data_2: "serialization_format_name_2",
value_type_attribute_name_for_value_data_3: ["path name", ...] <3>
# ...
}
end
end
class AttributeValueType < Lutaml::Model::Serializable
attribute :value_type_attribute_name_for_key, :string
attribute :value_type_attribute_name_for_value_data_1, :string
attribute :value_type_attribute_name_for_value_data_2, SomeType
attribute :value_type_attribute_name_for_value_data_3, MoreType
...
end
<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.
.Using map
with root_mappings
(key
and complex value
) to map a keyed collection into individual models
[example]
[source,yaml]
"vase1":
type: "vase"
details:
name: "Imperial Vase"
insignia: "Tang Tianbao"
urn:
primary: "urn:ceramic:vase:vase1"
"bowl2":
type: "bowl"
details:
name: "18th Century Bowl"
insignia: "Ming Wanli"
urn:
primary: "urn:ceramic:bowl:bowl2"
A model can be defined for this YAML as follows:
[source,ruby]
This is a normal Lutaml::Model class
class CeramicDetails < Lutaml::Model::Serializable
attribute :name, :string
attribute :insignia, :string
key_value do
map 'name', to: :name
map 'insignia', to: :insignia
end
end
This is a normal Lutaml::Model class
class Ceramic < Lutaml::Model::Serializable
attribute :ceramic_id, :string
attribute :ceramic_type, :string
attribute :ceramic_details, CeramicDetails
attribute :ceramic_urn, :string
key_value do
map 'id', to: :ceramic_id
map 'type', to: :ceramic_type
map 'details', to: :ceramic_details
map 'urn', to: :ceramic_urn
end
end
This is Lutaml::Model class that represents the collection of Ceramic objects
class CeramicCollection < Lutaml::Model::Serializable
attribute :ceramics, Ceramic, collection: true
key_value do
map to: :ceramics, # All data goes to the ceramics
attribute
root_mappings: {
# The key of an object in this collection is mapped to the ceramic_id
# attribute of the Ceramic object.
# (e.g. vase1
, bowl2
)
ceramic_id: :key,
ceramic_type: :type,
ceramic_details: "details",
ceramic_urn: ["urn", "primary"]
}
end
end
The output becomes:
[source,ruby]
ceramics_collection = CeramicCollection.from_yaml(yaml)
=> #<CeramicCollection:0x0000000107a2cf30
@ceramics=
[#<Ceramic:0x0000000107a2cf30
@ceramic_id="vase1",
@ceramic_type="vase",
@ceramic_details=
#<CeramicDetails:0x0000000107a2cf30
@name="Imperial Vase",
@insignia="Tang Tianbao">,
@ceramic_urn="urn:ceramic:vase:vase1">,
#<Ceramic:0x0000000107a2cf30
@ceramic_id="bowl2",
@ceramic_type="bowl",
@ceramic_details=
#<CeramicDetails:0x0000000107a2cf30
@name="18th Century Bowl",
@insignia="Ming Wanli">
@ceramic_urn="urn:ceramic:bowl:bowl2">]
first_ceramic = ceramics_collection.ceramics.first
puts first_ceramic.to_yaml
=>
---
id: vase1
type: vase
details:
name: Imperial Vase
insignia: Tang Tianbao
urn: urn:ceramic:vase:vase1
new_collection = CeramicCollection.new(ceramics: [
Ceramic.new(ceramic_id: "vase1",
ceramic_type: "vase",
ceramic_urn: "urn:ceramic:vase:vase1",
ceramic_details: CeramicDetails.new(
name: "Imperial Vase", insignia: "Tang Tianbao")
),
Ceramic.new(ceramic_id: "bowl2",
ceramic_type: "bowl",
ceramic_urn: "urn:ceramic:vase:bowl2",
ceramic_details: CeramicDetails.new(
name: "18th Century Bowl", insignia: "Ming Wanli")
)
])
new_collection.to_yaml
---
vase1:
type: vase
details:
name: Imperial Vase
insignia: Tang Tianbao
urn:
primary: urn:ceramic:vase:vase1
bowl2:
type: bowl
details:
name: 18th Century Bowl
insignia: Ming Wanli
urn:
primary: urn:ceramic:bowl:bowl2
====
===== 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:
[source,ruby]
class SomeKeyedCollection < Lutaml::Model::Serializable
attribute :name_of_attribute, AttributeValueType, collection: true
json | yaml | toml | key_value do
map to: :name_of_attribute,
root_mappings: {
value_type_attribute_name_for_key: :key, <1>
value_type_attribute_name_for_value: :value <2>
}
end
end
class AttributeValueType < Lutaml::Model::Serializable
attribute :value_type_attribute_name_for_key, :string
attribute :value_type_attribute_name_for_value, SomeObject
end
<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.
.Using map
with root_mappings
(key
and value
) to map a keyed collection into individual models
[example]
Given this data:
[source,yaml]
vase1:
name: Imperial Vase
insignia: "Tang Tianbao"
bowl2:
name: 18th Century Bowl
insignia: "Ming Wanli"
A model can be defined for this YAML as follows:
[source,ruby]
This is a normal Lutaml::Model class
class CeramicDetails < Lutaml::Model::Serializable
attribute :name, :string
attribute :insignia, :string
key_value do
map 'name', to: :name
map 'insignia', to: :insignia
end
end
This is a normal Lutaml::Model class
class Ceramic < Lutaml::Model::Serializable
attribute :ceramic_id, :string
attribute :ceramic_details, CeramicDetails
key_value do
map 'id', to: :ceramic_id
map 'details', to: :ceramic_details
end
end
This is Lutaml::Model class that represents the collection of Ceramic objects
class CeramicCollection < Lutaml::Model::Serializable
attribute :ceramics, Ceramic, collection: true
key_value do
map to: :ceramics, # All data goes to the ceramics
attribute
root_mappings: {
# The key of an object in this collection is mapped to the ceramic_id
# attribute of the Ceramic object.
# (e.g. vase1
, bowl2
)
ceramic_id: :key,
# The value of an object in this collection is mapped to the
# ceramic_details attribute of the Ceramic object.
# (e.g. name: 18th Century Bowl
, insignia: "Ming Wanli"
ceramic_details: :value
}
end
end
[source,ruby]
Parsing the YAML collection with dynamic data keys
ceramic_collection = CeramicCollection.from_yaml(yaml)
#<CeramicCollection:0x0000000104ac7240
@ceramics=
[#<Ceramic:0x0000000104ac6e30
@ceramic_id="vase1",
@ceramic_details=
#<CeramicDetails:0x0000000104ac6e30
@name="Imperial Vase",
@insignia="Tang Tianbao">,
#<Ceramic:0x0000000104ac58f0
@ceramic_id="bowl2",
@ceramic_details=
#<CeramicDetails:0x0000000104ac58f0
@name="18th Century Bowl",
@insignia="Ming Wanli">]
NOTE: When an individual Ceramic object is serialized, the id
attribute is
the original key in the incoming YAML data.
first_ceramic = ceramic_collection.ceramics.first
puts first_ceramic.to_yaml
=>
---
id: vase1
details:
name: Imperial Vase
insignia: Tang Tianbao
NOTE: When in a collection, the ceramic_id
attribute is used to key the data,
and it disappears from the individual object.
puts ceramic_collection.to_yaml
=>
---
vase1:
name: Imperial Vase
insignia: Tang Tianbao
bowl2:
name: 18th Century Bowl
insignia: Ming Wanli
NOTE: When the collection is serialized, the ceramic_id
attribute is used to
key the data. This is defined through the map
with root_mappings
method in
CeramicCollection.
new_collection = CeramicCollection.new(ceramics: [
Ceramic.new(ceramic_id: "vase1",
ceramic_details: CeramicDetails.new(
name: "Imperial Vase", insignia: "Tang Tianbao")
),
Ceramic.new(ceramic_id: "bowl2",
ceramic_details: CeramicDetails.new(
name: "18th Century Bowl", insignia: "Ming Wanli")
)
])
puts new_collection.to_yaml
=>
---
vase1:
name: Imperial Vase
insignia: Tang Tianbao
bowl2:
name: 18th Century Bowl
insignia: Ming Wanli
====
[[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 (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:
[source,ruby]
class SomeObject < Lutaml::Model::Serializable
attribute :name_of_attribute, AttributeValueType, collection: true
json | yaml | toml | key_value do
map 'key_value_model_attribute_name', to: :name_of_attribute,
child_mappings: {
value_type_attribute_name_1: <1>
{path_to_value_1}, <2>
value_type_attribute_name_2:
{path_to_value_2},
# ...
}
end
end
<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.
.Determining the path to value in a key-value data model
[example]
The following JSON contains 2 keys in schema named engine
and gearbox
.
[source,json]
{
"components": {
"engine": {
"manufacturer": "Ford",
"model": "V8"
},
"gearbox": {
"manufacturer": "Toyota",
"model": "4-speed"
}
}
}
The path to value for the engine
schema is [:components, :engine]
and for
the gearbox
schema is [:components, :gearbox]
.
In path_to_value
, the :key
and :value
are reserved instructions used to
assign the key or value of the serialization data respectively as the value to
the attribute.
[example]
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.
[source,json]
{
"components": {
"engine": { /.../ },
"gearbox": { /.../ }
}
}
====
If a specified value path is not found, the corresponding attribute in the model
will be assigned a nil
value.
.Attribute values set to 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
.
[source,json]
====
The following JSON contains 2 keys in schema named foo
and bar
.
[source,json]
{
"schemas": {
"foo": { <1>
"path": { <2>
"link": "link one",
"name": "one"
}
},
"bar": { <1>
"path": { <2>
"link": "link two",
"name": "two"
}
}
}
}
<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:
[source,ruby]
class Schema < Lutaml::Model::Serializable
attribute :id, :string
attribute :link, :string
attribute :name, :string
end
class ChildMappingClass < Lutaml::Model::Serializable
attribute :schemas, Schema, collection: true
json do
map "schemas", to: :schemas,
child_mappings: {
id: :key,
link: %i[path link],
name: %i[path name],
}
end
end
The output becomes:
[source,ruby]
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.
====
[[separate-serialization-model]]
=== Separate serialization model
The Serialize
module can be used to define only serialization mappings for a
separately defined model (a Ruby class).
Syntax:
[source,ruby]
class Foo < Lutaml::Model::Serializable
model {DataModelClass}
...
end
[example]
.Using the model
method to define serialization mappings for a separate model
[source,ruby]
class Ceramic
attr_accessor :type, :glaze
def name
"#{type} with #{glaze}"
end
end
class CeramicSerialization < Lutaml::Model::Serializable
model Ceramic
xml do
map_element 'type', to: :type
map_element 'glaze', to: :glaze
end
end
[source,ruby]
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
====
=== Rendering empty attributes and collections
By default, empty attributes and collections are not rendered in the output.
To render empty attributes and collections, use the render_nil
option.
Syntax:
[source,ruby]
xml do
map_element 'key_value_model_attribute_name', to: :name_of_attribute, render_nil: true
end
[source,ruby]
json | yaml | toml do
map 'key_value_model_attribute_name', to: :name_of_attribute, render_nil: true
end
.Using the render_nil
option to render empty attributes
[example]
[source,ruby]
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
json do
map 'type', to: :type, render_nil: true
map 'glaze', to: :glaze
end
end
[source,ruby]
Ceramic.new.to_json
{ 'type': null }
Ceramic.new(type: "Porcelain", glaze: "Clear").to_json
{ 'type': 'Porcelain', 'glaze': 'Clear' }
[source,ruby]
Ceramic.new.to_xml
Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
PorcelainClear
====
.Using the render_nil
option to render empty attribute collections
[example]
[source,ruby]
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
json do
map 'type', to: :type, render_nil: true
map 'glazes', to: :glazes, render_nil: true
end
end
[source,ruby]
Ceramic.new.to_json
{ 'type': null, 'glazes': [] }
Ceramic.new(type: "Porcelain", glazes: ["Clear"]).to_json
{ 'type': 'Porcelain', 'glazes': ['Clear'] }
[source,ruby]
Ceramic.new.to_xml
Ceramic.new(type: "Porcelain", glazes: ["Clear"]).to_xml
PorcelainClear
====
=== 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:
[source,ruby]
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
json | yaml | toml | key_value do
map 'name_of_attribute', to: :name_of_attribute, render_default: true
end
.Using the render_default
option to force encoding the default value
[example]
[source,ruby]
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
json do
map 'color', to: :color
map 'opacity', to: :opacity, render_default: true
map 'temperature', to: :temperature
map 'firingTime', to: :firing_time, render_default: true
end
end
====
.Attributes with render_default: true
are rendered when the value is identical to the default
[example]
[source,ruby]
glaze_new = Glaze.new
puts glaze_new.to_xml
Opaque
puts glaze_new.to_json
{"firingTime":60,"opacity":"Opaque"}
====
.Attributes with render_default: true
with non-default values are rendered
[example]
[source,ruby]
glaze = Glaze.new(color: 'Celadon', opacity: 'Semitransparent', temperature: 1300, firing_time: 90)
puts glaze.to_xml
Semitransparent
puts glaze.to_json
{"color":"Celadon","temperature":1300,"firingTime":90,"opacity":"Semitransparent"}
====
=== Advanced attribute mapping
==== Multiple mappings to single attribute
The mapping methods support multiple names mapping to a single attribute using
an array of names.
Syntax:
[source,ruby]
json | yaml | toml | key_value do
map ["name1", "name2"], to: :attribute_name
end
xml do
map_element ["name1", "name2"], to: :attribute_name
map_attribute ["attr1", "attr2"], to: :attribute_name
end
When serializing, the first element in the array of mapped names is always used
as the output name.
.Using multiple names to map to a single attribute
[example]
[source,ruby]
class CustomModel < Lutaml::Model::Serializable
attribute :full_name, Lutaml::Model::Type::String
attribute :color, Lutaml::Model::Type::String
attribute :id, Lutaml::Model::Type::String
json do
map ["name", "custom_name"], with: { to: :name_to_json, from: :name_from_json }
map ["color", "shade"], with: { to: :color_to_json, from: :color_from_json }
end
xml do
root "CustomModel"
map_element ["name", "custom-name"], with: { to: :name_to_xml, from: :name_from_xml }
map_element ["color", "shade"], with: { to: :color_to_xml, from: :color_from_xml }
map_attribute ["id", "identifier"], to: :id
end
Custom methods for JSON
def name_to_json(model, doc)
doc["name"] = "JSON Model: #{model.full_name}"
end
def name_from_json(model, value)
model.full_name = value&.sub(/^JSON Model: /, "")
end
def color_to_json(model, doc)
doc["color"] = model.color.upcase
end
def color_from_json(model, value)
model.color = value&.downcase
end
Custom methods for XML
def name_to_xml(model, parent, doc)
el = doc.create_element("name")
doc.add_text(el, "XML Model: #{model.full_name}")
doc.add_element(parent, el)
end
def name_from_xml(model, value)
model.full_name = value.sub(/^XML Model: /, "")
end
def color_to_xml(model, parent, doc)
el = doc.create_element("color")
doc.add_text(el, model.color.upcase)
doc.add_element(parent, el)
end
def color_from_xml(model, value)
model.color = value.downcase
end
end
For JSON:
[source,json]
{
"custom_name": "JSON Model: Vase",
"shade": "BLUE",
"identifier": "123"
}
For XML:
[source,xml]
XML Model: Vase
BLUE
----
[source,ruby]
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:
[source,ruby]
xml | json | yaml | toml do
map 'key_value_model_attribute_name', to: :name_of_attribute, delegate: :model_to_delegate_to
end
.Using the delegate
option to map attributes to nested objects
[example]
The following class will parse the JSON snippet below:
[source,ruby]
class Glaze < Lutaml::Model::Serializable
attribute :color, :string
attribute :temperature, :integer
json do
map 'color', to: :color
map 'temperature', to: :temperature
end
end
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, Glaze
json do
map 'type', to: :type
map 'color', to: :color, delegate: :glaze
end
end
[source,json]
{
"type": "Porcelain",
"color": "Clear"
}
[source,ruby]
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:
.
=== Value Transformations
The transform
option allows defining import/export transformations at both attribute and mapping levels:
[source,ruby]
class Person < Lutaml::Model::Serializable
Attribute-level transformation
attribute :name, :string, transform: {
export: ->(value) { value.upcase },
import: ->(value) { value.downcase }
}
Mapping-level transformation in JSON format
json do
map "fullName", to: :name, transform: {
export: ->(value) { "Dr. #{value}" },
import: ->(value) { value.gsub("Dr. ", "") }
}
end
Mapping-level transformation in XML format
xml do
map "full-name", to: :name, transform: {
export: ->(value) { "Dr. #{value}" },
import: ->(value) { value.gsub("Dr. ", "") }
}
end
end
The transformation precedence is:
- Mapping-level transformation if defined
- Attribute-level transformation if no mapping transformation exists
This allows flexible value transformations without needing format-specific custom methods:
[source,ruby]
person = Person.new(name: "john")
Uses mapping transformation
person.to_json # => {"fullName": "Dr. john"}
Person.from_json({ "fullName" => "Dr. john"}.to_json).name # => john
Uses attribute transformation when no mapping exists
person.to_yaml # => name: "JOHN"
The transform
option supports:
export
: Transform value during serializationimport
: Transform value during deserialization- Collections with array transformations
- Chaining of attribute and mapping transformations
==== 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:
.XML serialization with custom methods
[source,ruby]
xml do
map_element 'element_name', to: :name_of_element, with: {
to: :method_name_to_serialize,
from: :method_name_to_deserialize
}
map_attribute 'attribute_name', to: :name_of_attribute, with: {
to: :method_name_to_serialize,
from: :method_name_to_deserialize
}
map_content, to: :name_of_content, with: {
to: :method_name_to_serialize,
from: :method_name_to_deserialize
}
end
.Using the with:
key to define custom serialization methods for XML
[example]
The following class will parse the XML snippet below:
[source,ruby]
class Metadata < Lutaml::Model::Serializable
attribute :category, :string
attribute :identifier, :string
end
class CustomCeramic < Lutaml::Model::Serializable
attribute :name, :string
attribute :size, :integer
attribute :description, :string
attribute :metadata, Metadata
xml do
map_element "Name", to: :name, with: { to: :name_to_xml, from: :name_from_xml }
map_attribute "Size", to: :size, with: { to: :size_to_xml, from: :size_from_xml }
map_content with: { to: :description_to_xml, from: :description_from_xml }
map_element :metadata, to: :metadata, with: { to: :metadata_to_xml, from: :metadata_from_xml }
end
def name_to_xml(model, parent, doc)
el = doc.create_element("Name")
doc.add_text(el, "XML Masterpiece: #{model.name}")
doc.add_element(parent, el)
end
def name_from_xml(model, value)
model.name = value.sub(/^XML Masterpiece: /, "")
end
def size_to_xml(model, parent, doc)
doc.add_attribute(parent, "Size", model.size + 3)
end
def size_from_xml(model, value)
model.size = value.to_i - 3
end
def description_to_xml(model, parent, doc)
doc.add_text(parent, "XML Description: #{model.description}")
end
def description_from_xml(model, value)
model.description = value.join.strip.sub(/^XML Description: /, "")
end
def metadata_to_xml(model, parent, doc)
metadata_el = doc.create_element("metadata")
category_el = doc.create_element("category")
identifier_el = doc.create_element("identifier")
doc.add_text(category_el, model.metadata.category)
doc.add_text(identifier_el, model.metadata.identifier)
doc.add_element(metadata_el, category_el)
doc.add_element(metadata_el, identifier_el)
doc.add_element(parent, metadata_el)
end
def metadata_from_xml(model, value)
model.metadata ||= Metadata.new
model.metadata.category = value["elements"]["category"].text
model.metadata.identifier = value["elements"]["identifier"].text
end
end
[source,xml]
XML Masterpiece: Vase
XML Description: A beautiful ceramic vase
Metadata
123
----
[source,ruby]
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
XML Masterpiece: Vase
Glaze
15
XML Description: A beautiful vase
[source,ruby]
def custom_method_from_xml(model, value)
instance = value.node # Lutaml::Model::XmlAdapter::AdapterElement
OR
instance = value.node.adapter_node # Adapter::Element
xml = instance.to_xml
end
When building a model from XML in custom methods, if the value
parameter is a mapping_hash
, then it allows access to the parsed XML structure through value.node
which can be converted to an XML string using to_xml
.
NOTE: For NokogiriAdapter
, we can also call to_xml
on value.node.adapter_node
.
[source,ruby]
value
{"text"=>["\n ", "\n ", "\n "], "elements"=>{"category"=>{"text"=>"Metadata"}}}
value.to_xml
undefined_method to_xml
value.node
Nokogiri Adapter Node
#<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656ed8
@attributes={},
@children=
[#<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656cd0 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">,
#<Lutaml::Model::XmlAdapter::NokogiriElement:0x00000001076569b0
@attributes={},
@children=
[#<Lutaml::Model::XmlAdapter::NokogiriElement:0x00000001076567f8 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
@default_namespace=nil,
@name="category",
@namespace_prefix=nil,
@text="Metadata">,
#<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656028 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">],
@default_namespace=nil,
@name="metadata",
@namespace_prefix=nil,
@text="\n Metadata\n ">
Ox Adapter Node
#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584f78
@attributes={},
@children=
[#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584e60
@attributes={},
@children=[#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584d48 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
@default_namespace=nil,
@name="category",
@namespace_prefix=nil,
@text="Metadata">],
@default_namespace=nil,
@name="metadata",
@namespace_prefix=nil,
@text=nil>
Oga Adapter Node
<Lutaml::Model::XmlAdapter::Oga::Element:0x0000000107314158
@attributes={},
@children=
[#<Lutaml::Model::XmlAdapter::Oga::Element:0x0000000107314090 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">,
#<Lutaml::Model::XmlAdapter::Oga::Element:0x000000010730fe78
@attributes={},
@children=[#<Lutaml::Model::XmlAdapter::Oga::Element:0x000000010730fd88 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
@default_namespace=nil,
@name="category",
@namespace_prefix=nil,
@text="Metadata">,
#<Lutaml::Model::XmlAdapter::Oga::Element:0x000000010730f8d8 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">],
@default_namespace=nil,
@name="metadata",
@namespace_prefix=nil,
@text="\n Metadata\n ">
value.node.to_xml
#Metadata
====
==== Separate Serialization Model With Custom Methods
[example]
The following class will parse the XML snippet below:
[source,ruby]
class CustomModelChild
attr_accessor :street, :city
end
class CustomModelChildMapper < Lutaml::Model::Serializable
model CustomModelChild
attribute :street, Lutaml::Model::Type::String
attribute :city, Lutaml::Model::Type::String
xml do
map_element :street, to: :street
map_element :city, to: :city
end
end
class CustomModelParentMapper < Lutaml::Model::Serializable
attribute :first_name, Lutaml::Model::Type::String
attribute :child_mapper, CustomModelChildMapper
xml do
map_element :first_name, to: :first_name
map_element :CustomModelChild,
with: { to: :child_to_xml, from: :child_from_xml }
end
def child_to_xml(model, parent, doc)
child_el = doc.create_element("CustomModelChild")
street_el = doc.create_element("street")
city_el = doc.create_element("city")
doc.add_text(street_el, model.child_mapper.street)
doc.add_text(city_el, model.child_mapper.city)
doc.add_element(child_el, street_el)
doc.add_element(child_el, city_el)
doc.add_element(parent, child_el)
end
def child_from_xml(model, value)
model.child_mapper ||= CustomModelChild.new
model.child_mapper.street = value["elements"]["street"].text
model.child_mapper.city = value["elements"]["city"].text
end
end
[source,xml]
John
Oxford Street
London
----
[source,ruby]
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
NOTE: For custom models, to_xml
is called on the mapper class, not on model instance.
===== Key-value data model serialization with custom methods
.Key-value data model serialization with custom methods
[source,ruby]
json | yaml | toml do
map 'attribute_name', to: :name_of_attribute, with: {
to: :method_name_to_serialize,
from: :method_name_to_deserialize
}
end
.Using the with:
key to define custom serialization methods
[example]
The following class will parse the JSON snippet below:
[source,ruby]
class CustomCeramic < Lutaml::Model::Serializable
attribute :name, :string
attribute :size, :integer
json do
map 'name', to: :name, with: { to: :name_to_json, from: :name_from_json }
map 'size', to: :size
end
def name_to_json(model, doc)
doc["name"] = "Masterpiece: #{model.name}"
end
def name_from_json(model, value)
model.name = value.sub(/^Masterpiece: /, '')
end
end
[source,json]
{
"name": "Masterpiece: Vase",
"size": 12
}
[source,ruby]
CustomCeramic.from_json(json)
#<CustomCeramic:0x0000000104ac7240 @name="Vase", @size=12>
CustomCeramic.new(name: "Vase", size: 12).to_json
#{"name"=>"Masterpiece: Vase", "size"=>12}
====
== Importing data models
=== General
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.
.Importing an XML Schema model to create LutaML core models
[source]
╔════════════════════════════╗ ╔═══════════════════════╗
║ Serialization Models ║ ║ Core Model ║
╚════════════════════════════╝ ╚═══════════════════════╝
╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆ XML Schema (XSD/RNG/RNC) ┆ ┆ Model ┆
┆ │ ┆ ┌────────────────┐ ┆ │ ┆
┆ ┌──────┴──────┐ ┆ │ │ ┆ ┌────────┴──┐ ┆
┆ │ │ ┆ │ Model │ ┆ │ │ ┆
┆ Models Value Types ┆──►│ Importing │──►┆ Models Value Types ┆
┆ │ │ ┆ │ │ ┆ │ │ ┆
┆ │ │ ┆ └────────────────┘ ┆ │ │ ┆
┆ ┌────┴────┐ ┌─┴─┐ ┆ │ ┆ │ ┌──────┴──┐ ┆
┆ │ │ │ │ ┆ │ ┆ │ │ │ ┆
┆ Element Value xs:string ┆ │ ┆ │ String Integer ┆
┆ Attribute Type xs:date ┆ │ ┆ │ Date Float ┆
┆ Union Complex xs:boolean ┆ │ ┆ │ Time Boolean ┆
┆ Sequence Choice xs:anyURI ┆ │ ┆ │ ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯ │ ┆ └──────┐ ┆
│ ┆ │ ┆
│ ┆ Contains ┆
│ ┆ more Models ┆
│ ┆ (recursive) ┆
│ ┆ ┆
│ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
│ ┌────────────────┐
│ │ │
│ │ Model │
└──────────► │ Transformation │
│ & │
│ Mapping Rules │
│ │
└────────────────┘
[[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:
[source,ruby]
Lutaml::Model::Schema.from_xml(
xsd_schema, <1>
options: options <2>
)
<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.
[example]
.Using Lutaml::Model::Schema#from_xml
to convert an XML Schema to model files
[source,ruby]
xsd_schema = <<~XSD
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
/* your schema here */
</xs:schema>
XSD
options = {
These are all optional:
output_dir: 'path/to/directory',
namespace: 'http://example.com/namespace',
prefix: "example-prefix",
location: "http://example.com/example.xsd",
or
location: "path/to/schema/directory"
create_files: true, # Default: false
OR
load_classes: true, # Default: false
}
generates the files in the output_dir | default_dir
Lutaml::Model::Schema.from_xml(xsd_schema, options: options)
====
You could also directly load the generated Ruby files into your application by
requiring them.
[example]
.Using the generated Ruby files in your application
[source,ruby]
Lutaml::Model::Schema.from_xml(xsd_schema, options: {output_dir: 'path/to/directory'})
require_relative 'path/to/directory/*.rb'
====
== 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 range
[example]
The following class will validate the degree_settings
attribute to ensure that
it has at least one element and that the description
attribute is one of the
values in the set [one, two, three]
.
[source,ruby]
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
=> [
#<Lutaml::Model::CollectionSizeError: degree_settings must have at least 1 element>,
#<Lutaml::Model::ValueError: description must be one of [one, two, three]>,
#<Lutaml::Model::ChoiceUpperBoundError: Attribute count exceeds the upper bound>
]
e = klin.validate!
=> Lutaml::Model::ValidationError: [
degree_settings must have at least 1 element,
description must be one of [one, two, three],
Attribute count exceeds the upper bound
]
e.errors
=> [
#<Lutaml::Model::CollectionSizeError: degree_settings must have at least 1 element>,
#<Lutaml::Model::ValueError: description must be one of [one, two, three]>,
#<Lutaml::Model::ChoiceUpperBoundError: Attribute count exceeds the upper bound>
#<Lutaml::Model::ChoiceLowerBoundError: Attribute count is less than lower bound>
]
====
=== Custom validation
To add custom validation, override the validate
method in the model class.
Additional errors should be added to the errors
array.
[example]
The following class validates the degree_settings
attribute when the type
is
glass
to ensure that the value is less than 1300.
[source,ruby]
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
=> [#<Lutaml::Model::Error: Degree settings for glass must be less than 1300>]
====
== 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.
- All attributes are accessible in the Liquid template by their names.
- Nested attributes are also converted into
Liquid::Drop
objects so
inner attributes can be accessed using the Liquid dot notation.
NOTE: Every Lutaml::Model::Serializable
class extends the Liquefiable
module
which generates a corresponding Liquid::Drop
class.
NOTE: Methods defined in the Lutaml::Model::Serializable
class are not
accessible in the Liquid template.
.Using to_liquid
to convert model instances into corresponding Liquid drop instances
[example]
[source,ruby]
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
Ceramic::CeramicDrop
puts ceramic_drop.name
"Porcelain Vase"
puts ceramic_drop.temperature
1200
====
.Accessing LutaML::Model objects within a Liquid template
[example]
[source,ruby]
class Ceramic < Lutaml::Model::Serializable
attribute :name, :string
attribute :temperature, :integer
end
class CeramicCollection < Lutaml::Model::Serializable
attribute :ceramics, Ceramic, collection: true
end
sample.yml
:
[source,yaml]
ceramics:
- name: Porcelain Vase
temperature: 1200
- name: Earthenware Pot
temperature: 950
- name: Stoneware Jug
temperature: 1200
template.liquid
:
[source,liquid]
{% for ceramic in ceramic_collection.ceramics %}
- Name: "{{ ceramic.name }}"
** Temperature: {{ ceramic.temperature }}
{%- endfor %}
[source,ruby]
Load the Lutaml::Model collection
ceramic_collection = CeramicCollection.from_yaml(File.read("sample.yml"))
Load the Liquid template
template = Liquid::Template.parse(File.read("template.liquid"))
Pass the Lutaml::Model collection to the Liquid template and render
output = template.render("ceramic_collection" => ceramic_collection)
puts output
>
* Name: "Porcelain Vase"
** Temperature: 1200
* Name: "Earthenware Pot"
** Temperature: 950
* Name: "Stoneware Jug"
** Temperature: 1200
====
.Accessing nested LutaML::Model objects within nested Liquid templates
[example]
[source,ruby]
class Glaze < Lutaml::Model::Serializable
attribute :color, :string
attribute :opacity, :string
end
class CeramicWork < Lutaml::Model::Serializable
attribute :name, :string
attribute :glaze, Glaze
end
class CeramicCollection < Lutaml::Model::Serializable
attribute :ceramics, Ceramic, collection: true
end
ceramic_work = CeramicWork.new({
name: "Celadon Bowl",
glaze: Glaze.new({
color: "Jade Green",
opacity: "Translucent"
})
})
ceramic_work_drop = ceramic_work.to_liquid
CeramicWork::CeramicWorkDrop
puts ceramic_work_drop.name
"Celadon Bowl"
puts ceramic_work_drop.glaze.color
"Jade Green"
puts ceramic_work_drop.glaze.opacity
"Translucent"
ceramics.yml
:
[source,yaml]
ceramics:
- name: Celadon Bowl
glaze:
color: Jade Green
opacity: Translucent
- name: Earthenware Pot
glaze:
color: Rust Red
opacity: Opaque
- name: Stoneware Jug
glaze:
color: Cobalt Blue
opacity: Transparent
templates/_ceramics.liquid
:
[source,liquid]
{% for ceramic in ceramic_collection.ceramics %}
{% render 'ceramic' ceramic: ceramic %}
{%- endfor %}
NOTE: render
is a Liquid tag that renders a partial template, by default
Liquid uses the pattern _%s.liquid
to find the partial template. Here
ceramic
refers to the file at templates/_ceramic.liquid
.
templates/_ceramic.liquid
:
[source,liquid]
- Name: "{{ ceramic.name }}"
** Temperature: {{ ceramic.temperature }}
{%- if ceramic.glaze %}
** Glaze (color): {{ ceramic.glaze.color }}
** Glaze (opacity): {{ ceramic.glaze.opacity }}
{%- endif %}
[source,ruby]
require 'liquid'
Create a Liquid template object that supports dynamic loading
template = Liquid::Template.new
Link the Liquid template object to a "local file system" (directory)
file_system = Liquid::LocalFileSystem.new('templates/')
template.registers[:file_system] = file_system
Load the partial template, this is necessary.
This will also allow Liquid to load any inner partials from the file system
dynamically (see file_system.pattern
to see what it loads)
template.parse(file_system.read_template_file('ceramics'))
Read the lutaml-model collection
ceramic_collection = CeramicCollection.from_yaml(File.read("ceramics.yml"))
Render the template with the collection
output = template.render("ceramic_collection" => ceramic_collection)
puts output
>
* Name: "Celadon Bowl"
** Temperature: 1200
** Glaze (color): Jade Green
** Glaze (finish): Translucent
* Name: "Earthenware Pot"
** Temperature: 950
** Glaze (color): Rust Red
** Glaze (finish): Opaque
* Name: "Stoneware Jug"
** Temperature: 1200
** Glaze (color): Cobalt Blue
** Glaze (finish): Transparent
====
== Adapters
=== General
Lutaml::Model uses an adapter pattern to support multiple libraries for each
serialization format.
Lutaml::Model supports the following serialization formats:
You will need to specify the configuration for the adapter you want to use. The
easiest way is to copy and paste the following configuration into your code.
The configuration is as follows:
[source,ruby]
require 'lutaml/model'
require 'lutaml/model/xml_adapter/nokogiri_adapter'
require 'lutaml/model/json_adapter/standard_json_adapter'
require 'lutaml/model/toml_adapter/toml_rb_adapter'
require 'lutaml/model/yaml_adapter/standard_yaml_adapter'
Lutaml::Model::Config.configure do |config|
config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
config.yaml_adapter = Lutaml::Model::YamlAdapter::StandardYamlAdapter
config.json_adapter = Lutaml::Model::JsonAdapter::StandardJsonAdapter
config.toml_adapter = Lutaml::Model::TomlAdapter::TomlRbAdapter
end
You can also provide the adapter type by using symbols like
[source,ruby]
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
config.xml_adapter_type = :nokogiri # can be one of [:nokogiri, :ox, :oga]
config.yaml_adapter_type = :standard_yaml
config.json_adapter_type = :standard_json # can be one of [:standard_json, :multi_json]
config.toml_adapter_type = :toml_rb # can be one of [:toml_rb, :tomlib]
end
NOTE: By default yaml_adapter_type
and json_adapter_type
are set to
:standard_yaml
and :standard_json
respectively.
=== 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.
.Using the Nokogiri XML adapter
[source,ruby]
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
require 'lutaml/model/xml_adapter/nokogiri_adapter'
config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
end
.Using the Oga XML adapter
[source,ruby]
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
require 'lutaml/model/xml_adapter/oga_adapter'
config.xml_adapter = Lutaml::Model::XmlAdapter::OgaAdapter
end
.Using the Ox XML adapter
[source,ruby]
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
require 'lutaml/model/xml_adapter/ox_adapter'
config.xml_adapter = Lutaml::Model::XmlAdapter::OxAdapter
end
=== YAML
Lutaml::Model supports only one YAML adapter.
YAML::
(default)
The Psych YAML parser and emitter for Ruby.
Included in the Ruby standard library.
.Using the YAML adapter
[source,ruby]
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
require 'lutaml/model/yaml_adapter/standard_yaml_adapter'
config.yaml_adapter = Lutaml::Model::YamlAdapter::StandardYamlAdapter
end
=== 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.
.Using the JSON adapter
[source,ruby]
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
require 'lutaml/model/json_adapter/standard_json_adapter'
config.json_adapter = Lutaml::Model::JsonAdapter::StandardJsonAdapter
end
.Using the MultiJson adapter
[source,ruby]
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
require 'lutaml/model/json_adapter/multi_json_adapter'
config.json_adapter = Lutaml::Model::JsonAdapter::MultiJsonAdapter
end
=== 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.
.Using the Toml-rb adapter
[source,ruby]
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
require 'lutaml/model/toml_adapter/toml_rb_adapter'
config.toml_adapter = Lutaml::Model::TomlAdapter::TomlRbAdapter
end
.Using the Tomlib adapter
[source,ruby]
require 'lutaml/model'
Lutaml::Model::Config.configure do |config|
config.toml_adapter = Lutaml::Model::TomlAdapter::TomlibAdapter
require 'lutaml/model/toml_adapter/tomlib_adapter'
end
== 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.
[source,xml]
My name is
John Doe,
and I'm 28
years old
| 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
>>
- ComplexTypes are compiled to Lutaml::Model::Serializable classes containing the attributes.
- SimpleTypes are compiled to Lutaml::Model::Type::Value classes to support XML Schema level validations.
| Yes, Provides only an array of the classes and doesn't support
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.
|===
[[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.
[source,ruby]
class Example < Lutaml::Model::Serializable
...
end
[NOTE]
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.
[source,ruby]
class Example
include Lutaml::Model::Serialize
...
end
====
Shale uses Shale::Mapper
as the base inheritance class.
[source,ruby]
class Example < Shale::Mapper
...
end
Actions:
- Replace mentions of
Shale::Mapper
with Lutaml::Model::Serializable
. - Potentially replace inheritance with inclusion for suitable cases.
=== Step 2: Replace value type definitions
Value types in Lutaml::Model
are under the Lutaml::Model::Type
module,
or use the LutaML type symbols.
[source,ruby]
class Example < Lutaml::Model::Serializable
attribute :length, :integer
attribute :description, :string
end
[NOTE]
Lutaml::Model
supports specifying predefined value types as strings or
symbols, which is not supported by Shale.
[source,ruby]
class Example < Lutaml::Model::Serializable
attribute :length, Lutaml::Model::Type::Integer
attribute :description, "String"
end
====
Value types in Shale are under the Shale::Type
module.
[source,ruby]
class Example < Shale::Mapper
attribute :length, Shale::Type::Integer
attribute :description, Shale::Type::String
end
Action:
- Replace mentions of
Shale::Type
with Lutaml::Model::Type
. - Potentially replace value type definitions with strings or symbols.
=== Step 3: Configure serialization adapters
Lutaml::Model
uses a configuration block to set the serialization adapters.
[source,ruby]
require 'lutaml/model/xml_adapter/nokogiri_adapter'
Lutaml::Model::Config.configure do |config|
config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
end
The equivalent for Shale is this:
[source,ruby]
require 'shale/adapter/nokogiri'
Shale.xml_adapter = Shale::Adapter::Nokogiri
Here are places that this code may reside at:
- If your code is a standalone Ruby script, this code will be present in your code.
- If your code is organized in a Ruby gem, this code will be specified somewhere referenced by
lib/your_gem_name.rb
. - If your code contains tests or specs, they will be in the test setup file, e.g. RSpec
spec/spec_helper.rb
.
Actions:
=== 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.
[source,ruby]
class Example < Lutaml::Model::Serializable
attribute :name, :string
attribute :size, :integer
attribute :color, :string
attribute :description, :string
json do
map "name", to: :name, with: { to: :name_to_json, from: :name_from_json }
map "size", to: :size
map "color", to: :color,
with: { to: :color_to_json, from: :color_from_json }
map "description", to: :description,
with: { to: :description_to_json, from: :description_from_json }
end
xml do
root "CustomSerialization"
map_element "Name", to: :name,
with: { to: :name_to_xml, from: :name_from_xml }
map_attribute "Size", to: :size
map_element "Color", to: :color,
with: { to: :color_to_xml, from: :color_from_xml }
map_content to: :description,
with: { to: :description_to_xml,
from: :description_from_xml }
end
def name_to_json(model, doc)
doc["name"] = "JSON Masterpiece: #{model.name}"
end
def name_from_json(model, value)
model.name = value.sub(/^JSON Masterpiece: /, "")
end
def color_to_json(model, doc)
doc["color"] = model.color.upcase
end
def color_from_json(model, value)
model.color = value.downcase
end
def description_to_json(model, doc)
doc["description"] = "JSON Description: #{model.description}"
end
def description_from_json(model, value)
model.description = value.sub(/^JSON Description: /, "")
end
def name_to_xml(model, parent, doc)
el = doc.create_element("Name")
doc.add_text(el, "XML Masterpiece: #{model.name}")
doc.add_element(parent, el)
end
def name_from_xml(model, value)
model.name = value.sub(/^XML Masterpiece: /, "")
end
def color_to_xml(model, parent, doc)
color_element = doc.create_element("Color")
doc.add_text(color_element, model.color.upcase)
doc.add_element(parent, color_element)
end
def color_from_xml(model, value)
model.color = value.downcase
end
def description_to_xml(model, parent, doc)
doc.add_text(parent, "XML Description: #{model.description}")
end
def description_from_xml(model, value)
model.description = value.join.strip.sub(/^XML Description: /, "")
end
end
Custom serialization methods in Shale do not map to specific attributes, but
allow the user to specify where the data goes.
[source,ruby]
class Example < Shale::Mapper
attribute :name, Shale::Type::String
attribute :size, Shale::Type::Integer
attribute :color, Shale::Type::String
attribute :description, Shale::Type::String
json do
map "name", using: { from: :name_from_json, to: :name_to_json }
map "size", to: :size
map "color", using: { from: :color_from_json, to: :color_to_json }
map "description", to: :description, using: { from: :description_from_json, to: :description_to_json }
end
xml do
root "CustomSerialization"
map_element "Name", using: { from: :name_from_xml, to: :name_to_xml }
map_attribute "Size", to: :size
map_element "Color", using: { from: :color_from_xml, to: :color_to_xml }
map_content to: :description, using: { from: :description_from_xml, to: :description_to_xml }
end
def name_to_json(model, doc)
doc['name'] = "JSON Masterpiece: #{model.name}"
end
def name_from_json(model, value)
model.name = value.sub(/^JSON Masterpiece: /, "")
end
def color_to_json(model, doc)
doc['color'] = model.color.upcase
end
def color_from_json(model, doc)
model.color = doc['color'].downcase
end
def description_to_json(model, doc)
doc['description'] = "JSON Description: #{model.description}"
end
def description_from_json(model, doc)
model.description = doc['description'].sub(/^JSON Description: /, "")
end
def name_from_xml(model, node)
model.name = node.text.sub(/^XML Masterpiece: /, "")
end
def name_to_xml(model, parent, doc)
name_element = doc.create_element('Name')
doc.add_text(name_element, model.street.to_s)
doc.add_element(parent, name_element)
end
end
NOTE: There are cases where the Shale implementation of custom methods work
differently from the Lutaml::Model implementation. In these cases, you will need
to adjust the custom methods accordingly.
Actions:
- Replace the
using
keyword with the with
keyword. - Adjust the custom methods.
== 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.