General
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:
attribute :name_of_attribute, TypeWhere,
name_of_attribute-
The defined name of the attribute.
Type-
The type of the attribute.
attribute class method to define simple attributesclass Studio < Lutaml::Model::Serializable
attribute :name, :string
attribute :address, :string
attribute :established, :date
ends = 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>Restricting the value of an attribute
The restrict class method is used to update or refine the validation rules for an attribute that has already been defined. This allows you to apply additional or stricter constraints to an existing attribute without redefining it.
restrict class method to update the options of an existing attributeclass Studio < Lutaml::Model::Serializable
attribute :name, :string
restrict :name, collection: 1..3, pattern: /[A-Za-z]+/
endclass Document < Lutaml::Model::Serializable
attribute :status, :string
end
class DraftDocument < Document
# Only allow "draft" or "in_review" as valid statuses for drafts
restrict :status, values: %w[draft in_review]
end
class PublishedDocument < Document
# Only allow "published" or "archived" as valid statuses for published documents
restrict :status, values: %w[published archived]
end
# Usage
# Call .validate! to trigger validation and raise an error if the value is not allowed
Document.new(status: "draft").validate! # valid, there are no validation rules for `Document`
Document.new(status: "published").validate! # valid, there are no validation rules for `Document`
DraftDocument.new(status: "draft").validate! # valid
DraftDocument.new(status: "in_review").validate! # valid
DraftDocument.new(status: "published").validate! # raises error (not allowed)
PublishedDocument.new(status: "published").validate! # valid
PublishedDocument.new(status: "archived").validate! # valid
PublishedDocument.new(status: "draft").validate! # raises error (not allowed)All options that are supported by the attribute class method are also supported by the restrict method. Any unsupported option passed to restrict will result in a Lutaml::Model::InvalidAttributeOptionsError being raised.
Polymorphic attributes
General
A polymorphic attribute is an attribute that can accept multiple types of values. This is useful when the attribute defines common characteristics and behaviors among different types.
An attribute with a defined value type also accepts values that are of a class that is a subclass of the defined type.
The assigned attribute of Type accepts polymorphic classes as long as the assigned instance is of a class that either inherits from the declared type or matches it.
Naïve approach does not work…
A naïve polymorphic approach is to define an attribute with a superclass type and assign instances of subclasses to it.
While this approach works (somewhat) in modeling, it does not work with serialization (half) or deserialization (not at all).
The following example illustrates why such approach is naïve.
class Studio < Lutaml::Model::Serializable
attribute :name, :string
end
# CeramicStudio is a specialization of Studio
class CeramicStudio < Studio
attribute :clay_type, :string
end
class PotteryClass < Lutaml::Model::Serializable
# the :studio attribute should accept Studio and CeramicStudio
attribute :studio, Studio
end# This works
> s = Studio.new(name: 'Pottery Studio')
> p = PotteryClass.new(studio: s)
> p.studio
# => <Studio:0x0000000104ac7240 @name="Pottery Studio", @address=nil, @established=nil>
# A subclass of Studio is also valid
> s = CeramicStudio.new(name: 'Ceramic World', clay_type: 'Red')
> p = PotteryClass.new(studio: s)
> p.studio
# => <CeramicStudio:0x0000000104ac7240 @name="Ceramic World", @address=nil, @established=nil, @clay_type="Red">
> p.studio.name
# => "Ceramic World"
> p.studio.clay_type
# => "Red"So far so good. However, this approach does not work in serialization. This is what happens when we call to_yaml on the PotteryClass instance.
> puts p.to_yaml
# => ---
# => studio:
# => name: Ceramic World
# => clay_type: RedWhen deserializing the YAML string, the studio attribute will be deserialized as an instance of Studio, not CeramicStudio. This means that the clay_type attribute will be lost.
> p = PotteryClass.load_yaml("---\nstudio:\n name: Ceramic World\n clay_type: Red")
> p.studio
# => <Studio:0x0000000104ac7240 @name="Ceramic World">
> p.studio.clay_type
# => ERRORProper polymorphic approaches
Lutaml::Model offers rich support for polymorphic attributes, through configuration at both attribute and serialization levels.
In polymorphism, there are the following components:
- polymorphic attribute
-
the attribute that can be assigned multiple types.
- polymorphic attribute class
-
the class that has a polymorphic attribute.
- polymorphic superclass
-
a class assigned to a polymorphic attribute that serves as the superclass for all accepted polymorphic classes.
- polymorphic subclass
-
a class that is a subclass of the polymorphic superclass and can be assigned to the polymorphic attribute. There are often more than 2 subclasses in a scenario since polymorphism is meant to apply to multiple types.
To utilize polymorphic attributes, modification to all of these components are necessary.
In serialized form, polymorphic classes are differentiated by an explicit "polymorphic class differentiator".
In key-value formats like YAML, the polymorphic class differentiator is typically a key-value pair that contains the polymorphic class name.
references:
- _class: Document # This is a DocumentReference
name: "The Tibetan Book of the Dead"
document_id: "book:tbtd"
- _class: Anchor # This is an AnchorReference
name: "Chapter 1"
anchor_id: "book:tbtd:anchor-1"In XML, the polymorphic class differentiator is typically an attribute that contains the polymorphic class name.
<references>
<!-- The "document-ref" value is a DocumentReference -->
<reference reference-type="document-ref">
<name>The Tibetan Book of the Dead</name>
<document_id>book:tbtd</document_id>
</reference>
<!-- The "anchor-ref" value is an AnchorReference -->
<reference reference-type="anchor-ref">
<name>Chapter 1</name>
<anchor_id>book:tbtd:anchor-1</anchor_id>
</reference>
</references>| While it is possible to determine different polymorphic classes based on the attributes they contain, such mechanism would not be able to determine the polymorphic class if serializations of two polymorphic subclasses can be identical. |
There are two basic scenarios in using polymorphic attributes:
-
Scenario 1: Setting polymorphism in the polymorphic superclass:
-
Scenario 2: Setting polymorphism in the individual polymorphic subclasses:
Please refer to spec/lutaml/model/polymorphic_spec.rb for full examples of implementing polymorphic attributes. |
Defining the polymorphic attribute
The polymorphic attribute class is a class that has a polymorphic attribute.
At this level, the polymorphic option is used to specify the types that the polymorphic attribute can accept.
class PolymorphicAttributeClass < Lutaml::Model::Serializable
attribute :attribute_name, (1)
{polymorphic-superclass-class}, (2)
{options}, (3)
polymorphic: [ (4)
polymorphic-subclass-1, (5)
polymorphic-subclass-2,
]
end| 1 | The name of the polymorphic attribute. |
| 2 | The polymorphic superclass class. |
| 3 | Any options for the attribute. |
| 4 | The polymorphic option that determines the acceptable polymorphic subclasses, or just true. |
| 5 | The polymorphic subclasses. |
The polymorphic option is an array of polymorphic subclasses that the attribute can accept.
These options enable the following scenarios.
-
If the polymorphic attribute is to only contain instances of the
polymorphic-superclass-class, not its subclasses, then thepolymorphicoption is not needed.In the following code,
ReferenceSethas an attributereferencesthat only accepts instances ofReference. Thepolymorphicoption does not apply.class ReferenceSet < Lutaml::Model::Serializable attribute :references, Reference, collection: true end -
If the attribute (collection or not) is meant to only contain one type of polymorphic subclasses, then the
polymorphicoption is also not needed, because the polymorphic subclass can be stated as the attribute value type.In the following code,
ReferenceSethas an attributereferencesthat only accepts instances ofDocumentReference, a subclass ofReference. Thepolymorphicoption does not apply.class ReferenceSet < Lutaml::Model::Serializable attribute :references, DocumentReference, collection: true end -
If the attribute (collection or not) is meant to contain instances belonging to any polymorphic subclass of a defined base class, then set the
polymorphic: trueoption.In the following code,
ReferenceSetis a class that has a polymorphic attributereferences. Thereferencesattribute can accept instances of any polymorphic subclass of theReferencebase class, sopolymorphic: trueis set.class ReferenceSet < Lutaml::Model::Serializable attribute :references, Reference, collection: true, polymorphic: true end -
If the attribute (collection or not) is meant to contain instances belonging to more than one polymorphic subclass, then those acceptable polymorphic subclasses should be explicitly specified in the
polymorphic: […]option.In the following code,
ReferenceSetis a class that has a polymorphic attributereferences. Thereferencesattribute can accept instances ofDocumentReferenceandAnchorReference, both of which are subclasses ofReference.class ReferenceSet < Lutaml::Model::Serializable attribute :references, Reference, collection: true, polymorphic: [ DocumentReference, AnchorReference, ] end
Differentiating polymorphic subclasses
General
A polymorphic subclass needs an additional attribute with the polymorphic_class option to allow Lutaml::Model for identifying itself in serialization. This attribute is called the "polymorphic class differentiator".
There are two methods for setting the polymorphic class differentiator:
-
Setting the polymorphic class differentiator in the polymorphic superclass, as polymorphic subclasses inherit from it (relying on [model-inheritance]).
-
Setting the polymorphic class differentiator in the individual polymorphic subclasses
Setting the differentiator in the polymorphic superclass
The polymorphic class differentiator can be set in the polymorphic superclass. This scenario fits best if there are many polymorphic subclasses and the polymorphic superclass can be modified.
Syntax:
class PolymorphicSuperclass < Lutaml::Model::Serializable
attribute :{_polymorphic_differentiator}, (1)
:string, (2)
polymorphic_class: true (3)
# ...
end| 1 | The polymorphic differentiator is a normal attribute that can be assigned to any name. |
| 2 | The polymorphic differentiator must have a value type of :string. |
| 3 | The option for polymorphic_class must be set to true to indicate that this attribute accepts subclass types. |
Setting the differentiator in the individual polymorphic subclasses
The polymorphic class differentiator can be set in the individual polymorphic subclasses. This scenario fits best if there are few polymorphic subclasses and the polymorphic superclass cannot be modified.
Syntax:
# No modification to the superclass is needed.
class PolymorphicSuperclass < Lutaml::Model::Serializable
# ...
end
# The polymorphic differentiator is set in the subclass.
class PolymorphicSubclass < PolymorphicSuperclass
attribute
:{_polymorphic_differentiator}, (1)
:string, (2)
polymorphic_class: true (3)
# ...
end| 1 | The polymorphic differentiator is a normal attribute that can be assigned to any name. |
| 2 | The polymorphic differentiator must have a value type of :string. |
| 3 | The option for polymorphic_class must be set to true to indicate that this attribute accepts subclass types. |
Polymorphic differentiation in serialization
General
The polymorphic attribute class needs to determine what class to use based on the serialized value of the polymorphic differentiator.
The polymorphic attribute class mapping is format-independent, allowing for differentiation of polymorphic subclasses in different serialization formats.
The mapping of the serialized polymorphic differentiator can be set in either:
-
the polymorphic superclass; or
-
the polymorphic attribute class and the individual polymorphic subclasses.
Mapping in the polymorphic superclass
This use case applies when the polymorphic superclass can be modified, and that polymorphism is intended to apply to all its subclasses.
This is done through the polymorphic_map option in the serialization blocks inside the polymorphic attribute class.
Syntax:
class PolymorphicSuperclass < Lutaml::Model::Serializable
attribute :{_polymorphic_differentiator}, :string, polymorphic_class: true
xml do
(map_attribute | map_element) "XmlPolymorphicAttributeName", (1)
to: :{_polymorphic_differentiator}, (2)
polymorphic_map: { (3)
"xml-value-for-subclass-1" => PolymorphicSubclass1, (4)
"xml-value-for-subclass-2" => PolymorphicSubclass2,
}
end
(key_value | key_value_format) do
map "KeyValuePolymorphicAttributeName", (5)
to: :{_polymorphic_differentiator}, (6)
polymorphic_map: {
"keyvalue-value-for-subclass-1" => PolymorphicSubclass1,
"keyvalue-value-for-subclass-2" => PolymorphicSubclass2,
}
end
end
class PolymorphicSubclass1 < PolymorphicSuperclass
# ...
end
class PolymorphicSubclass2 < PolymorphicSuperclass
# ...
end
class PolymorphicAttributeClass < Lutaml::Model::Serializable
attribute :polymorphic_attribute,
PolymorphicSuperclass,
{options},
polymorphic: [
PolymorphicSubclass1,
PolymorphicSubclass2,
]
# ...
end| 1 | The name of the XML element or attribute that contains the polymorphic differentiator. |
| 2 | The name of the polymorphic differentiator attribute defined in attribute with the polymorphic option. |
| 3 | The polymorphic_map option that determines the class to use based on the value of the differentiator. |
| 4 | The mapping of the differentiator value to the polymorphic subclass. |
| 5 | The name of the key-value element that contains the polymorphic differentiator. |
| 6 | The name of the polymorphic differentiator attribute defined in attribute with the polymorphic option. |
class Reference < Lutaml::Model::Serializable
attribute :_class, :string, polymorphic_class: true
attribute :name, :string
xml do
map_attribute "reference-type", to: :_class, polymorphic_map: {
"document-ref" => "DocumentReference",
"anchor-ref" => "AnchorReference",
}
map_element "name", to: :name
end
key_value do
map "_class", to: :_class, polymorphic_map: {
"Document" => "DocumentReference",
"Anchor" => "AnchorReference",
}
map "name", to: :name
end
end
class DocumentReference < Reference
attribute :document_id, :string
xml do
map_element "document_id", to: :document_id
end
key_value do
map "document_id", to: :document_id
end
end
class AnchorReference < Reference
attribute :anchor_id, :string
xml do
map_element "anchor_id", to: :anchor_id
end
key_value do
map "anchor_id", to: :anchor_id
end
end
class ReferenceSet < Lutaml::Model::Serializable
attribute :references, Reference, collection: true, polymorphic: [
DocumentReference,
AnchorReference,
]
end---
references:
- _class: Document
name: The Tibetan Book of the Dead
document_id: book:tbtd
- _class: Anchor
name: Chapter 1
anchor_id: book:tbtd:anchor-1<ReferenceSet>
<references reference-type="document-ref">
<name>The Tibetan Book of the Dead</name>
<document_id>book:tbtd</document_id>
</references>
<references reference-type="anchor-ref">
<name>Chapter 1</name>
<anchor_id>book:tbtd:anchor-1</anchor_id>
</references>
</ReferenceSet>Mapping in the polymorphic attribute class and individual polymorphic subclasses
This use case applies when the polymorphic superclass is not meant to be modified.
This is done through the polymorphic_map option in the serialization blocks inside the polymorphic attribute class, and the polymorphic option in the individual polymorphic subclasses.
In this scenario, similar to the previous case where the polymorphic differentiator is set at the polymorphic superclass, the following conditions must be satisifed:
-
the polymorphic differentiator attribute name must be the same across polymorphic subclasses
If the model polymorphic differentiator in one polymorphic subclass is
_ref_type, then it must be so in all other polymorphic subclasses. -
the polymorphic differentiator in the serialization formats must be identical within the polymorphic subclasses of that serialization format.
If the XML polymorphic differentiator is
reference-type, then it must be so in the XML of all polymorphic subclasses.
Syntax:
# Assume that we have no access to the base class and we need to define
# polymorphism in the sub-classes.
class PolymorphicSuperclass < Lutaml::Model::Serializable
end
class PolymorphicSubclass1 < PolymorphicSuperclass
attribute :_polymorphic_differentiator, :string
xml do
(map_attribute | map_element) "XmlPolymorphicAttributeName", (1)
to: :_polymorphic_differentiator
end
(key_value | key_value_format) do
map "KeyValuePolymorphicAttributeName", (2)
to: :_polymorphic_differentiator
end
end
class PolymorphicSubclass2 < PolymorphicSuperclass
attribute :_polymorphic_differentiator, :string
xml do
(map_attribute | map_element) "XmlPolymorphicAttributeName2",
to: :_polymorphic_differentiator
end
(key_value | key_value_format) do
map "KeyValuePolymorphicAttributeName2",
to: :_polymorphic_differentiator
end
end
class PolymorphicAttributeClass < Lutaml::Model::Serializable
attribute :polymorphic_attribute,
PolymorphicSuperclass,
{options},
polymorphic: [
PolymorphicSubclass1,
PolymorphicSubclass2,
] (3)
# ...
xml do
map_element "XmlPolymorphicElement", (4)
to: :polymorphic_attribute,
polymorphic: { (5)
# This refers to the polymorphic differentiator attribute in the polymorphic subclass.
attribute: :_polymorphic_differentiator, (6)
class_map: { (7)
"xml-i-am-subclass-1" => "PolymorphicSubclass1",
"xml-i-am-subclass-2" => "PolymorphicSubclass2",
},
}
end
(key_value | key_value_format) do
map "KeyValuePolymorphicAttributeName", (8)
to: :polymorphic_attribute,
polymorphic: { (9)
attribute: :_polymorphic_differentiator, (10)
class_map: { (11)
"keyvalue-i-am-subclass-1" => "PolymorphicSubclass1",
"keyvalue-i-am-subclass-2" => "PolymorphicSubclass2",
},
}
end
end| 1 | The name of the XML element or attribute that contains the polymorphic differentiator. |
| 2 | The name of the key-value element that contains the polymorphic differentiator. |
| 3 | Definition of the polymorphic attribute and the polymorphic subclasses in the polymorphic attribute class. |
| 4 | The name of the XML element that contains the polymorphic attributes. This must be an element as a polymorphic attribute must be a model. |
| 5 | The polymorphic option on a mapping defines necessary information for polymorphic serialization. |
| 6 | The attribute: name of the polymorphic differentiator attribute defined in the polymorphic subclass. |
| 7 | The class_map: option that determines the polymorphic subclass to use based on the value of the differentiator. |
| 8 | The name of the key-value format key that contains the polymorphic attributes. |
| 9 | Same as <5>, but for the key-value format. |
| 10 | Same as <6>, but for the key-value format. |
| 11 | Same as <7>, but for the key-value format. |
class Reference < Lutaml::Model::Serializable
attribute :name, :string
end
class DocumentReference < Reference
attribute :_class, :string
attribute :document_id, :string
xml do
map_element "document_id", to: :document_id
map_attribute "reference-type", to: :_class
end
key_value do
map "document_id", to: :document_id
map "_class", to: :_class
end
end
class AnchorReference < Reference
attribute :_class, :string
attribute :anchor_id, :string
xml do
map_element "anchor_id", to: :anchor_id
map_attribute "reference-type", to: :_class
end
key_value do
map "anchor_id", to: :anchor_id
map "_class", to: :_class
end
end
class ReferenceSet < Lutaml::Model::Serializable
attribute :references, Reference, collection: true, polymorphic: [
DocumentReference,
AnchorReference,
]
xml do
root "ReferenceSet"
map_element "reference", to: :references, polymorphic: {
# This refers to the attribute in the polymorphic model, you need
# to specify the attribute name (which is specified in the sub-classed model).
attribute: "_class",
class_map: {
"document-ref" => "DocumentReference",
"anchor-ref" => "AnchorReference",
},
}
end
key_value do
map "references", to: :references, polymorphic: {
attribute: "_class",
class_map: {
"Document" => "DocumentReference",
"Anchor" => "AnchorReference",
},
}
end
end---
references:
- _class: Document
name: The Tibetan Book of the Dead
document_id: book:tbtd
- _class: Anchor
name: Chapter 1
anchor_id: book:tbtd:anchor-1<ReferenceSet>
<reference reference-type="document-ref">
<name>The Tibetan Book of the Dead</name>
<document_id>book:tbtd</document_id>
</reference>
<reference reference-type="anchor-ref">
<name>Chapter 1</name>
<anchor_id>book:tbtd:anchor-1</anchor_id>
</reference>
</ReferenceSet>Collection attributes
Define attributes as collections (arrays or hashes) to store multiple values using the collection option.
When defining a collection attribute, it is important to understand the default initialization behavior and how to customize it.
By default, collections are initialized as nil. However, if you want the collection to be initialized as an empty array, you can use the initialize_empty: true option.
collection can be set to:
true-
The attribute contains an unbounded collection of objects of the declared class.
{min}..{max}-
The attribute contains a collection of objects of the declared class with a count within the specified range. If the number of objects is out of this numbered range, a
CollectionCountOutOfRangeErrorwill be raised.When set to
0..1, it means that the attribute is optional, it could be empty or contain one object of the declared class.When set to
1..(equivalent to1..Infinity), it means that the attribute must contain at least one object of the declared class and can contain any number of objects.When set to 5..10` means that there is a minimum of 5 and a maximum of 10 objects of the declared class. If the count of values for the attribute is less then 5 or greater then 10, the
CollectionCountOutOfRangeErrorwill be raised.
Syntax:
attribute :name_of_attribute, Type, collection: true
attribute :name_of_attribute, Type, collection: {min}..{max}
attribute :name_of_attribute, Type, collection: {min}..collection option to define a collection attributeclass Studio < Lutaml::Model::Serializable
attribute :location, :string
attribute :potters, :string, collection: true
attribute :address, :string, collection: 1..2
attribute :hobbies, :string, collection: 0..
end> Studio.new
> # address count is `0`, must be between 1 and 2 (Lutaml::Model::CollectionCountOutOfRangeError)
> Studio.new({ address: ["address 1", "address 2", "address 3"] })
> # address count is `3`, must be between 1 and 2 (Lutaml::Model::CollectionCountOutOfRangeError)
> Studio.new({ address: ["address 1"] }).potters
> # []
> Studio.new({ address: ["address 1"] }).address
> # ["address 1"]
> Studio.new(address: ["address 1"], potters: ['John Doe', 'Jane Doe']).potters
> # ['John Doe', 'Jane Doe']# Default to `nil`
class SomeModel < Lutaml::Model::Serializable
attribute :coll, :string, collection: true
xml do
root "some-model"
map_element 'collection', to: :coll
end
key_value do
map 'collection', to: coll
end
end
puts SomeModel.new.coll
# => nil
puts SomeModel.new.to_xml
# =>
# <some-model xsi:xmlns="..."><collection xsi:nil="true"/></some-model>
puts SomeModel.new.to_yaml
# =>
# ---
# coll: null# Default to empty array
class SomeModel < Lutaml::Model::Serializable
attribute :coll, :string, collection: true, initialize_empty: true
xml do
map_element 'collection', to: :coll
end
key_value do
map 'collection', to: coll
end
end
puts SomeModel.new.coll
# => []
puts SomeModel.new.to_xml
# =>
# <some-model><collection/></some-model>
puts SomeModel.new.to_yaml
# =>
# ---
# coll: []Derived attributes
A derived attribute has a value computed dynamically on evaluation of an instance method.
It is defined using the method: option along with a mandatory type specification. If the return value is not of the type, it will be casted to the specified type.
Syntax:
attribute :name_of_attribute, Type, method: :instance_method_nameclass Invoice < Lutaml::Model::Serializable
attribute :subtotal, :float
attribute :tax, :float
attribute :total, :float, 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.0Choice attributes
The choice directive allows specifying that elements from the specified range are included.
Attribute-level definitions are supported. This can be used with both key_value and xml mappings. |
Syntax:
choice(min: {min}, max: {max}) do
{block}
endWhere,
min-
The minimum number of elements that must be included. The minimum value can be
0. max-
The maximum number of elements that can be included. The maximum value can go up to
Float::INFINITY. block-
The block of elements that must be included. The block can contain multiple
attributeandchoicedirectives.
choice directive to define a set of attributes with a rangeclass Studio < Lutaml::Model::Serializable
choice(min: 1, max: 3) do
choice(min: 1, max: 2) do
attribute :prefix, :string
attribute :forename, :string
end
attribute :completeName, :string
end
endThis 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
prefixandforenameattributes or just theforenameattribute. -
The last attribute
completeNameis optional.
Choice and sequence can be used together to create complex structures.
choice (model-level) and sequence (XML-level) directives togetherclass Person < Lutaml::Model::Serializable
attribute :first_name, :string
attribute :last_name, :string
choice do
attribute :age, :integer
attribute :dob, :string
end
choice(min: 1, max: 2) do
attribute :email, :string, collection: 0..2
attribute :phone, :string, collection: 0..2
attribute :address, :string, collection: true
end
xml do
root "Person", mixed: true
sequence do
map_element :FirstName, to: :first_name
map_element :LastName, to: :last_name
map_element :Age, to: :age
map_element :Dob, to: :dob
map_element :Email, to: :email
map_element :Phone, to: :phone
map_element :Address, to: :address
end
end
endChoosing between Custom Types and Transform Procs
When deciding how to implement value transformations, consider:
Use Custom Value Type classes when:
-
Bidirectional transformations are needed across formats
-
Format-specific representations are required (XML wants YYYYMMDD, JSON wants DDMMYYYY)
-
The logic will be reused across multiple attributes or models
-
Complex parsing or calculation is involved (e.g., ISO week date calculations)
-
Type safety and encapsulation are important
Use Attribute-level transform procs when:
-
Same simple transformation applies to ALL serialization formats uniformly
-
Logic is specific to one attribute in one model (non-reusable)
-
Quick inline modification is sufficient
-
No format-specific behavior is needed
Use Mapping-level transform procs when:
-
Different transformation needed per serialization format
-
One-off, non-reusable transformation
-
Combined with attribute-level transforms for multi-stage processing
See Value Transformations Guide for complete decision matrix and examples.
The choice directive can be used with import_model_attributes. For more details, see Using import_model_attributes inside a choice block. |