lutaml-model 0.6.7 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-repos-todo.json +7 -0
- data/.github/workflows/dependent-repos.json +17 -9
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +51 -65
- data/Gemfile +4 -1
- data/README.adoc +5083 -2612
- data/RELEASE_NOTES.adoc +346 -0
- data/docs/custom_adapters.adoc +144 -0
- data/lib/lutaml/model/attribute.rb +101 -16
- data/lib/lutaml/model/choice.rb +7 -0
- data/lib/lutaml/model/comparable_model.rb +48 -9
- data/lib/lutaml/model/config.rb +48 -42
- data/lib/lutaml/model/error/collection_count_out_of_range_error.rb +1 -1
- data/lib/lutaml/model/error/polymorphic_error.rb +14 -0
- data/lib/lutaml/model/error.rb +1 -0
- data/lib/lutaml/model/format_registry.rb +41 -0
- data/lib/lutaml/model/hash/document.rb +11 -0
- data/lib/lutaml/model/hash/mapping.rb +19 -0
- data/lib/lutaml/model/hash/mapping_rule.rb +9 -0
- data/lib/lutaml/model/hash/standard_adapter.rb +17 -0
- data/lib/lutaml/model/hash/transform.rb +8 -0
- data/lib/lutaml/model/hash.rb +21 -0
- data/lib/lutaml/model/json/document.rb +11 -0
- data/lib/lutaml/model/json/mapping.rb +19 -0
- data/lib/lutaml/model/json/mapping_rule.rb +9 -0
- data/lib/lutaml/model/{json_adapter → json}/multi_json_adapter.rb +4 -5
- data/lib/lutaml/model/{json_adapter/standard_json_adapter.rb → json/standard_adapter.rb} +5 -3
- data/lib/lutaml/model/json/transform.rb +8 -0
- data/lib/lutaml/model/json.rb +21 -0
- data/lib/lutaml/model/key_value_document.rb +27 -0
- data/lib/lutaml/model/{key_value_mapping.rb → mapping/key_value_mapping.rb} +64 -16
- data/lib/lutaml/model/{key_value_mapping_rule.rb → mapping/key_value_mapping_rule.rb} +18 -2
- data/lib/lutaml/model/mapping/mapping.rb +13 -0
- data/lib/lutaml/model/mapping/mapping_rule.rb +300 -0
- data/lib/lutaml/model/schema/xml_compiler.rb +15 -15
- data/lib/lutaml/model/sequence.rb +2 -2
- data/lib/lutaml/model/serialization_adapter.rb +22 -0
- data/lib/lutaml/model/serialize.rb +219 -444
- data/lib/lutaml/model/services/logger.rb +54 -0
- data/lib/lutaml/model/services/transformer.rb +48 -0
- data/lib/lutaml/model/services.rb +2 -0
- data/lib/lutaml/model/toml/document.rb +11 -0
- data/lib/lutaml/model/toml/mapping.rb +27 -0
- data/lib/lutaml/model/toml/mapping_rule.rb +9 -0
- data/lib/lutaml/model/{toml_adapter → toml}/toml_rb_adapter.rb +3 -3
- data/lib/lutaml/model/toml/tomlib_adapter.rb +19 -0
- data/lib/lutaml/model/toml/transform.rb +8 -0
- data/lib/lutaml/model/toml.rb +30 -0
- data/lib/lutaml/model/transform/key_value_transform.rb +291 -0
- data/lib/lutaml/model/transform/xml_transform.rb +239 -0
- data/lib/lutaml/model/transform.rb +78 -0
- data/lib/lutaml/model/type/date.rb +1 -1
- data/lib/lutaml/model/type/date_time.rb +2 -2
- data/lib/lutaml/model/type/hash.rb +1 -1
- data/lib/lutaml/model/type/time.rb +2 -2
- data/lib/lutaml/model/type/time_without_date.rb +2 -2
- data/lib/lutaml/model/type/value.rb +6 -9
- data/lib/lutaml/model/uninitialized_class.rb +64 -0
- data/lib/lutaml/model/utils.rb +44 -0
- data/lib/lutaml/model/validation.rb +1 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model/{xml_adapter → xml}/builder/nokogiri.rb +2 -2
- data/lib/lutaml/model/{xml_adapter → xml}/builder/oga.rb +10 -10
- data/lib/lutaml/model/{xml_adapter → xml}/builder/ox.rb +1 -1
- data/lib/lutaml/model/{xml_adapter/xml_document.rb → xml/document.rb} +41 -21
- data/lib/lutaml/model/xml/element.rb +32 -0
- data/lib/lutaml/model/xml/mapping.rb +410 -0
- data/lib/lutaml/model/xml/mapping_rule.rb +141 -0
- data/lib/lutaml/model/xml/nokogiri_adapter.rb +232 -0
- data/lib/lutaml/model/{xml_adapter → xml}/oga/document.rb +1 -1
- data/lib/lutaml/model/{xml_adapter → xml}/oga/element.rb +3 -1
- data/lib/lutaml/model/xml/oga_adapter.rb +171 -0
- data/lib/lutaml/model/xml/ox_adapter.rb +215 -0
- data/lib/lutaml/model/xml/transform.rb +8 -0
- data/lib/lutaml/model/{xml_adapter → xml}/xml_attribute.rb +1 -1
- data/lib/lutaml/model/{xml_adapter → xml}/xml_element.rb +23 -10
- data/lib/lutaml/model/{xml_adapter → xml}/xml_namespace.rb +1 -1
- data/lib/lutaml/model/xml.rb +31 -0
- data/lib/lutaml/model/xml_adapter/element.rb +11 -25
- data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +6 -223
- data/lib/lutaml/model/xml_adapter/oga_adapter.rb +13 -163
- data/lib/lutaml/model/xml_adapter/ox_adapter.rb +10 -207
- data/lib/lutaml/model/yaml/document.rb +10 -0
- data/lib/lutaml/model/yaml/mapping.rb +19 -0
- data/lib/lutaml/model/yaml/mapping_rule.rb +9 -0
- data/lib/lutaml/model/{yaml_adapter/standard_yaml_adapter.rb → yaml/standard_adapter.rb} +4 -3
- data/lib/lutaml/model/yaml/transform.rb +8 -0
- data/lib/lutaml/model/yaml.rb +21 -0
- data/lib/lutaml/model.rb +40 -4
- data/lutaml-model.gemspec +0 -4
- data/spec/benchmarks/xml_parsing_benchmark_spec.rb +7 -7
- data/spec/fixtures/person.rb +5 -5
- data/spec/lutaml/model/attribute_spec.rb +37 -1
- data/spec/lutaml/model/cdata_spec.rb +9 -9
- data/spec/lutaml/model/collection_spec.rb +50 -2
- data/spec/lutaml/model/comparable_model_spec.rb +92 -27
- data/spec/lutaml/model/custom_bibtex_adapter_spec.rb +598 -0
- data/spec/lutaml/model/custom_vobject_adapter_spec.rb +1226 -0
- data/spec/lutaml/model/defaults_spec.rb +1 -1
- data/spec/lutaml/model/enum_spec.rb +1 -1
- data/spec/lutaml/model/group_spec.rb +333 -20
- data/spec/lutaml/model/hash/adapter_spec.rb +255 -0
- data/spec/lutaml/model/json_adapter_spec.rb +6 -6
- data/spec/lutaml/model/key_value_mapping_spec.rb +65 -3
- data/spec/lutaml/model/mixed_content_spec.rb +24 -24
- data/spec/lutaml/model/multiple_mapping_spec.rb +5 -5
- data/spec/lutaml/model/ordered_content_spec.rb +6 -6
- data/spec/lutaml/model/polymorphic_spec.rb +526 -0
- data/spec/lutaml/model/render_empty_spec.rb +194 -0
- data/spec/lutaml/model/render_nil_spec.rb +206 -22
- data/spec/lutaml/model/root_mappings_spec.rb +3 -3
- data/spec/lutaml/model/schema/xml_compiler_spec.rb +6 -6
- data/spec/lutaml/model/serializable_spec.rb +179 -103
- data/spec/lutaml/model/simple_model_spec.rb +9 -9
- data/spec/lutaml/model/toml_adapter_spec.rb +6 -6
- data/spec/lutaml/model/toml_spec.rb +51 -0
- data/spec/lutaml/model/transformation_spec.rb +72 -15
- data/spec/lutaml/model/uninitialized_class_spec.rb +96 -0
- data/spec/lutaml/model/value_map_spec.rb +240 -0
- data/spec/lutaml/model/xml/namespace/nested_with_explicit_namespace_spec.rb +85 -0
- data/spec/lutaml/model/xml/namespace_spec.rb +57 -0
- data/spec/lutaml/model/xml/xml_element_spec.rb +93 -0
- data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +2 -2
- data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +2 -2
- data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +2 -2
- data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +6 -6
- data/spec/lutaml/model/xml_adapter_spec.rb +6 -6
- data/spec/lutaml/model/xml_mapping_rule_spec.rb +105 -5
- data/spec/lutaml/model/xml_mapping_spec.rb +70 -16
- data/spec/lutaml/model/xml_spec.rb +63 -0
- data/spec/lutaml/model/yaml_adapter_spec.rb +3 -5
- data/spec/sample_model_spec.rb +3 -3
- data/spec/spec_helper.rb +3 -3
- metadata +76 -59
- data/lib/lutaml/model/json_adapter/json_document.rb +0 -20
- data/lib/lutaml/model/json_adapter/json_object.rb +0 -28
- data/lib/lutaml/model/loggable.rb +0 -15
- data/lib/lutaml/model/mapping_rule.rb +0 -109
- data/lib/lutaml/model/toml_adapter/toml_document.rb +0 -20
- data/lib/lutaml/model/toml_adapter/toml_object.rb +0 -28
- data/lib/lutaml/model/toml_adapter/tomlib_adapter.rb +0 -20
- data/lib/lutaml/model/toml_adapter.rb +0 -6
- data/lib/lutaml/model/xml_mapping.rb +0 -307
- data/lib/lutaml/model/xml_mapping_rule.rb +0 -122
- data/lib/lutaml/model/yaml_adapter/yaml_document.rb +0 -20
- data/lib/lutaml/model/yaml_adapter/yaml_object.rb +0 -28
- data/lib/lutaml/model/yaml_adapter.rb +0 -8
data/RELEASE_NOTES.adoc
ADDED
@@ -0,0 +1,346 @@
|
|
1
|
+
= Release Notes: v0.6.7 to v0.7.1
|
2
|
+
|
3
|
+
== Overview
|
4
|
+
|
5
|
+
This release introduces significant enhancements to the lutaml-model library, focusing on improved handling of missing values, comprehensive polymorphic model support, and important bug fixes. This document outlines the key changes and provides guidance for upgrading your codebase.
|
6
|
+
|
7
|
+
== Key changes
|
8
|
+
|
9
|
+
=== Missing values handling family
|
10
|
+
|
11
|
+
A comprehensive set of features for handling the "missing values" family (empty,
|
12
|
+
non-existent, and undefined values) across different serialization formats has
|
13
|
+
been introduced.
|
14
|
+
|
15
|
+
WARNING: The default behavior for handling uninitialized collection attributes
|
16
|
+
is changed from before.
|
17
|
+
|
18
|
+
In version 0.6.7, an uninitialized collection attribute in Lutaml::Model is
|
19
|
+
represented as an empty array (`[]`).
|
20
|
+
|
21
|
+
In version 0.7.1, an uninitialized collection attribute in Lutaml::Model is
|
22
|
+
represented as `nil`.
|
23
|
+
|
24
|
+
This change is made to ensure consistency across different serialization formats
|
25
|
+
and to provide a predictable and consistent behavior based on explicit
|
26
|
+
declarations when dealing with missing values.
|
27
|
+
|
28
|
+
Previously, Lutaml::Model had a limited ability to handle the missing value types,
|
29
|
+
for example, the appearance and round-trip behavior of the blank XML element
|
30
|
+
is uncertain. Lutaml::Model also did not support XML `nil` and YAML `null` values
|
31
|
+
in a consistent manner, nor the inconsistency where TOML does not support
|
32
|
+
the notion of `nil` or `null` values.
|
33
|
+
|
34
|
+
This change may break existing code that relies on the previous behavior. If you
|
35
|
+
were using uninitialized collection attributes in your code, you may need to
|
36
|
+
update your code to handle the new behavior.
|
37
|
+
|
38
|
+
There is now a series of features to support the missing values family, including:
|
39
|
+
|
40
|
+
* Attribute-level `initialize_empty` option, which controls the default
|
41
|
+
initialization behavior of collection attributes. Previously, collection
|
42
|
+
attributes defaulted to an empty array (`[]`) when uninitialized. The default
|
43
|
+
is now the value `nil`.
|
44
|
+
+
|
45
|
+
Revert to legacy behavior: You can return to the previous behavior by setting
|
46
|
+
`initialize_empty: true` in the attribute definition.
|
47
|
+
|
48
|
+
* Mapping-rule-level `render_nil` and `render_empty` options have been revamped
|
49
|
+
into accepting additional options that are used to control how nil and empty
|
50
|
+
values are parsed from and rendered in serialization formats.
|
51
|
+
+
|
52
|
+
The behavior of `render_nil: true` and `render_empty: true` is now equivalent to
|
53
|
+
`render_nil: :as_empty` and `render_empty: :as_empty`, respectively.
|
54
|
+
It is strongly recommended that the legacy behavior be replaced with the new
|
55
|
+
options.
|
56
|
+
+
|
57
|
+
The reason being that legacy behavior breaks round-trips between conversions of
|
58
|
+
the same format. In the case of `render_nil: true`, a model value of `nil` is
|
59
|
+
serialized into an XML blank element, but when the XML blank element is
|
60
|
+
deserialized, it is deserialized into an empty string (`""`, as per the W3C XML
|
61
|
+
standard), not `nil`.
|
62
|
+
|
63
|
+
* Mapping-rule-level `value_map` option, which provides fine-grained control
|
64
|
+
over how missing values are handled during serialization and deserialization.
|
65
|
+
|
66
|
+
* Support for a new `Uninitialized` value at the `Lutaml::Model` level, which is
|
67
|
+
used to represent uninitialized attributes.
|
68
|
+
+
|
69
|
+
This means that if you have a model with an attribute that is not set, it will
|
70
|
+
be represented as `Uninitialized` instead of `nil`. This is useful for
|
71
|
+
distinguishing between an attribute that is not set and an attribute that is
|
72
|
+
set to `nil`, and as a result will preserve the round-trip behavior of
|
73
|
+
serialization formats.
|
74
|
+
|
75
|
+
* Support for `nil` values in JSON and YAML, and XML serialization formats.
|
76
|
+
+
|
77
|
+
The `nil` value is now supported in JSON, YAML and XML serialization formats.
|
78
|
+
This means that if you have a model with an attribute that is set to `nil`, it
|
79
|
+
will be serialized as `null` in JSON/YAML and as an empty element in XML. This
|
80
|
+
is useful for representing missing values in a consistent manner across
|
81
|
+
different serialization formats.
|
82
|
+
|
83
|
+
|
84
|
+
|
85
|
+
==== `initialize_empty` option
|
86
|
+
|
87
|
+
The `initialize_empty` option controls the default initialization behavior of
|
88
|
+
collection attributes:
|
89
|
+
|
90
|
+
[source,ruby]
|
91
|
+
----
|
92
|
+
# Default to `nil`
|
93
|
+
class SomeModel < Lutaml::Model::Serializable
|
94
|
+
attribute :coll, :string, collection: true
|
95
|
+
|
96
|
+
xml do
|
97
|
+
root "some-model"
|
98
|
+
map_element 'collection', to: :coll
|
99
|
+
end
|
100
|
+
end
|
101
|
+
puts SomeModel.new.coll # => nil
|
102
|
+
|
103
|
+
# Default to empty array
|
104
|
+
class SomeModel < Lutaml::Model::Serializable
|
105
|
+
attribute :coll, :string, collection: true, initialize_empty: true
|
106
|
+
|
107
|
+
xml do
|
108
|
+
map_element 'collection', to: :coll
|
109
|
+
end
|
110
|
+
end
|
111
|
+
puts SomeModel.new.coll # => []
|
112
|
+
----
|
113
|
+
|
114
|
+
==== `value_map` option
|
115
|
+
|
116
|
+
The `value_map` option provides fine-grained control over how missing values are handled during serialization and deserialization. This is especially useful when different serialization formats represent missing values differently.
|
117
|
+
|
118
|
+
[source,ruby]
|
119
|
+
----
|
120
|
+
class ExampleClass < Lutaml::Model::Serializable
|
121
|
+
attribute :status, :string
|
122
|
+
|
123
|
+
xml do
|
124
|
+
map_element 'status', to: :status, value_map: {
|
125
|
+
from: { empty: :nil, omitted: :omitted, nil: :nil },
|
126
|
+
to: { empty: :nil, omitted: :omitted, nil: :nil }
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
json do
|
131
|
+
map 'status', to: :status, value_map: {
|
132
|
+
from: { empty: :nil, omitted: :omitted, nil: :nil },
|
133
|
+
to: { empty: :nil, omitted: :omitted, nil: :nil }
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
toml do
|
138
|
+
map 'status', to: :status, value_map: {
|
139
|
+
from: { empty: :nil, omitted: :omitted },
|
140
|
+
to: { empty: :nil, omitted: :omitted, nil: :omitted }
|
141
|
+
}
|
142
|
+
end
|
143
|
+
end
|
144
|
+
----
|
145
|
+
|
146
|
+
For collection attributes, the value_map behaves differently depending on the `initialize_empty` setting.
|
147
|
+
|
148
|
+
==== `render_nil` and `render_empty` modes
|
149
|
+
|
150
|
+
These options provide shorthand methods to control how nil and empty values are rendered in serialization formats:
|
151
|
+
|
152
|
+
* `render_nil: true | :as_empty | :as_blank | :nil | :omit` - Controls how nil values are rendered
|
153
|
+
* `render_empty: :as_empty | :as_blank | :nil | :omit` - Controls how empty collections are rendered
|
154
|
+
|
155
|
+
[source,ruby]
|
156
|
+
----
|
157
|
+
class SomeModel < Lutaml::Model::Serializable
|
158
|
+
attribute :coll, :string, collection: true
|
159
|
+
|
160
|
+
xml do
|
161
|
+
root "some-model"
|
162
|
+
map_element 'collection', to: :coll, render_nil: :omit
|
163
|
+
end
|
164
|
+
|
165
|
+
json do
|
166
|
+
map 'collection', to: :coll, render_empty: :as_nil
|
167
|
+
end
|
168
|
+
end
|
169
|
+
----
|
170
|
+
|
171
|
+
=== Polymorphic model support
|
172
|
+
|
173
|
+
From version 0.7.1, Lutaml::Model now supports polymorphic models for attribute
|
174
|
+
types.
|
175
|
+
|
176
|
+
Comprehensive support for polymorphic models has been introduced, allowing for
|
177
|
+
flexible modeling of inheritance relationships and proper
|
178
|
+
serialization/deserialization.
|
179
|
+
|
180
|
+
This means that you can define attributes that can accept multiple types of
|
181
|
+
objects, and the library will handle serialization and deserialization for these
|
182
|
+
types seamlessly.
|
183
|
+
|
184
|
+
Specifically, the following features have been added:
|
185
|
+
|
186
|
+
* Polymorphic attribute definition
|
187
|
+
* Polymorphic class differentiation in model and serializations
|
188
|
+
|
189
|
+
==== Polymorphic attribute definition
|
190
|
+
|
191
|
+
Polymorphic attributes can be defined using the `polymorphic` option.
|
192
|
+
|
193
|
+
It is possible to define polymorphic attribute classes in the superclass
|
194
|
+
and subclasses.
|
195
|
+
|
196
|
+
The `polymorphic` option can be set to a collection of classes, and the
|
197
|
+
`polymorphic_class` option can be set to `true` to indicate acceptance of any
|
198
|
+
subclass of the polymorphic class. Alternatively, you can specify a collection
|
199
|
+
of classes to restrict the accepted types.
|
200
|
+
|
201
|
+
[source,ruby]
|
202
|
+
----
|
203
|
+
class ReferenceSet < Lutaml::Model::Serializable
|
204
|
+
attribute :references, Reference, collection: true, polymorphic: [
|
205
|
+
DocumentReference,
|
206
|
+
AnchorReference,
|
207
|
+
]
|
208
|
+
end
|
209
|
+
----
|
210
|
+
|
211
|
+
When you are not requiring a specific set of subclasses, you can use the
|
212
|
+
`polymorphic: true` option to indicate that any subclass of the specified class is
|
213
|
+
acceptable.
|
214
|
+
|
215
|
+
[source,ruby]
|
216
|
+
----
|
217
|
+
class ReferenceSet < Lutaml::Model::Serializable
|
218
|
+
attribute :references, Reference, collection: true, polymorphic: true
|
219
|
+
end
|
220
|
+
----
|
221
|
+
|
222
|
+
|
223
|
+
==== Polymorphic class differentiator
|
224
|
+
|
225
|
+
When serializing polymorphic models, a differentiator attribute is required to
|
226
|
+
also be serialized to identify the specific subclass of the polymorphic class.
|
227
|
+
|
228
|
+
This differentiator attribute is typically a string that indicates the type of
|
229
|
+
the object being serialized. The differentiator attribute can be defined in the
|
230
|
+
superclass or subclasses of the polymorphic class.
|
231
|
+
|
232
|
+
Typically, the differentiator attribute is an XML element or attribute (e.g. `type="document-ref"`), or in JSON a `@`-prefixed key (e.g. `@type`).
|
233
|
+
|
234
|
+
A polymorphic differentiator attribute can be set in either the superclass or subclasses:
|
235
|
+
|
236
|
+
[source,ruby]
|
237
|
+
----
|
238
|
+
# In superclass
|
239
|
+
class Reference < Lutaml::Model::Serializable
|
240
|
+
attribute :_class, :string, polymorphic_class: true
|
241
|
+
# ...
|
242
|
+
end
|
243
|
+
|
244
|
+
# Or in subclasses
|
245
|
+
class DocumentReference < Reference
|
246
|
+
attribute :_class, :string, polymorphic_class: true
|
247
|
+
# ...
|
248
|
+
end
|
249
|
+
|
250
|
+
# Or in subclasses
|
251
|
+
class AnchorReference < Reference
|
252
|
+
attribute :_class, :string, polymorphic_class: true
|
253
|
+
# ...
|
254
|
+
end
|
255
|
+
----
|
256
|
+
|
257
|
+
Given the differentiator attribute being `_class`, we still need to define the
|
258
|
+
mapping for the differentiator attribute in the superclass or subclasses.
|
259
|
+
|
260
|
+
Polymorphic mapping in serialization is supported through the `polymorphic_map` option:
|
261
|
+
|
262
|
+
[source,ruby]
|
263
|
+
----
|
264
|
+
class Reference < Lutaml::Model::Serializable
|
265
|
+
attribute :_class, :string, polymorphic_class: true
|
266
|
+
|
267
|
+
xml do
|
268
|
+
map_attribute "reference-type", to: :_class, polymorphic_map: {
|
269
|
+
"document-ref" => "DocumentReference",
|
270
|
+
"anchor-ref" => "AnchorReference"
|
271
|
+
}
|
272
|
+
end
|
273
|
+
|
274
|
+
key_value do
|
275
|
+
map "_class", to: :_class, polymorphic_map: {
|
276
|
+
"Document" => "DocumentReference",
|
277
|
+
"Anchor" => "AnchorReference"
|
278
|
+
}
|
279
|
+
end
|
280
|
+
end
|
281
|
+
----
|
282
|
+
|
283
|
+
The `polymorphic_map` option is used to indicate that when serializing a
|
284
|
+
`DocumentReference` object, the `_class` attribute will be serialized as
|
285
|
+
`document-ref`, and when serializing an `AnchorReference` object, the `_class`
|
286
|
+
attribute will be serialized as `anchor-ref`.
|
287
|
+
|
288
|
+
This is a mapping-level option so that it can be used in serialization formats
|
289
|
+
independently.
|
290
|
+
|
291
|
+
This will produce a differentiator attribute in the serialized output as such.
|
292
|
+
|
293
|
+
[source,yaml]
|
294
|
+
----
|
295
|
+
---
|
296
|
+
references:
|
297
|
+
- _class: Document
|
298
|
+
# other attributes...
|
299
|
+
- _class: Anchor
|
300
|
+
# other attributes...
|
301
|
+
----
|
302
|
+
|
303
|
+
|
304
|
+
=== Importable model improvements
|
305
|
+
|
306
|
+
Importable model functionality has been improved, with better support for reusable models:
|
307
|
+
|
308
|
+
* `import_model` - Imports both attributes and mappings
|
309
|
+
* `import_model_attributes` - Imports only attributes
|
310
|
+
* `import_model_mappings` - Imports only mappings
|
311
|
+
|
312
|
+
Bug fixes for the import_model functionality ensure more reliable model reuse.
|
313
|
+
|
314
|
+
=== Circular reference handling
|
315
|
+
|
316
|
+
Improved handling of circular references in the `ComparableModel` module prevents stack overflow errors when comparing models with self-referential structures.
|
317
|
+
|
318
|
+
== Upgrade guide
|
319
|
+
|
320
|
+
=== Missing values handling
|
321
|
+
|
322
|
+
If you were previously using `render_nil: true`, you can continue using it, but
|
323
|
+
you may want to explore the more flexible `value_map` option for fine-grained
|
324
|
+
control over different serialization formats.
|
325
|
+
|
326
|
+
For collection attributes, consider whether you want collections to initialize
|
327
|
+
as `nil` or as an empty array by setting the `initialize_empty` option
|
328
|
+
accordingly.
|
329
|
+
|
330
|
+
=== Polymorphic models
|
331
|
+
|
332
|
+
If you were previously using class detection for polymorphic models without
|
333
|
+
explicit differentiators, you should now define a polymorphic differentiator
|
334
|
+
attribute and use the `polymorphic_class: true` option.
|
335
|
+
|
336
|
+
== Contributors
|
337
|
+
|
338
|
+
Thank you to all contributors who made this release possible, especially:
|
339
|
+
|
340
|
+
* HassanAkbar
|
341
|
+
* Ronald Tse
|
342
|
+
* suleman-uzair
|
343
|
+
|
344
|
+
== Compatibility
|
345
|
+
|
346
|
+
This release maintains compatibility with Ruby 2.7 and above.
|
@@ -0,0 +1,144 @@
|
|
1
|
+
= Custom Adapters in Lutaml::Model
|
2
|
+
|
3
|
+
Lutaml::Model provides a flexible system for creating custom adapters to handle different data formats. This guide explains how to create and use custom adapters in your application.
|
4
|
+
|
5
|
+
== Overview
|
6
|
+
|
7
|
+
Custom adapters allow you to extend Lutaml::Model to support additional data formats beyond the built-in ones (TOML, YAML, JSON, XML). Each adapter consists of three main components:
|
8
|
+
|
9
|
+
. An adapter class that handles parsing and serialization
|
10
|
+
. A mapping class that defines how data maps to your model
|
11
|
+
. A transform class that handles data transformation
|
12
|
+
|
13
|
+
== Creating a Custom Adapter
|
14
|
+
|
15
|
+
=== 1. Adapter Class
|
16
|
+
|
17
|
+
The adapter class is responsible for parsing input data and converting it back to the target format. It must implement:
|
18
|
+
|
19
|
+
* `self.parse(data, options = {})` - Class method to parse input data
|
20
|
+
* `to_<format_name>` - Instance method to convert data back to the target format
|
21
|
+
|
22
|
+
.Example of a custom adapter for parsing string pairs
|
23
|
+
[source,ruby]
|
24
|
+
----
|
25
|
+
class PairsAdapter < Lutaml::Model::KeyValueDocument
|
26
|
+
attr_reader :parsed_data
|
27
|
+
|
28
|
+
def initialize(parsed_data)
|
29
|
+
@parsed_data = parsed_data
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.parse(data, options = {})
|
33
|
+
# Example input: "name:John|age:30"
|
34
|
+
parsed = data.split("|").map { |pair| pair.split(":") }.to_h
|
35
|
+
new(parsed)
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_pairs_format
|
39
|
+
# Example output: "name:John|age:30"
|
40
|
+
parsed_data.map { |k, v| "#{k}:#{v}" }.join("|")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
----
|
44
|
+
|
45
|
+
=== 2. Mapping Class
|
46
|
+
|
47
|
+
The mapping class defines how data fields map to your model attributes. It should inherit from `Lutaml::Model::Mapping`.
|
48
|
+
|
49
|
+
[source,ruby]
|
50
|
+
----
|
51
|
+
class PairsMappingRule < Lutaml::Model::MappingRule
|
52
|
+
end
|
53
|
+
|
54
|
+
class PairsMapping < Lutaml::Model::Mapping
|
55
|
+
def map_field(name, to:)
|
56
|
+
@mappings << PairsMappingRule.new(name, to: to)
|
57
|
+
end
|
58
|
+
|
59
|
+
def mappings
|
60
|
+
@mappings
|
61
|
+
end
|
62
|
+
end
|
63
|
+
----
|
64
|
+
|
65
|
+
=== 3. Transform Class
|
66
|
+
|
67
|
+
The transform class handles the conversion between your data format and the model. It should inherit from `Lutaml::Model::Transform`.
|
68
|
+
|
69
|
+
[source,ruby]
|
70
|
+
----
|
71
|
+
class PairsTransform < Lutaml::Model::Transform
|
72
|
+
def self.data_to_model(context, data, format, options = {})
|
73
|
+
model = context.model.new
|
74
|
+
model.name = data["name"]
|
75
|
+
model.age = data["age"]
|
76
|
+
model
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.model_to_data(context, model, format, options = {})
|
80
|
+
{ "name" => model.name, "age" => model.age }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
----
|
84
|
+
|
85
|
+
== Registering Your Adapter
|
86
|
+
|
87
|
+
Register your custom adapter with the FormatRegistry:
|
88
|
+
|
89
|
+
[source,ruby]
|
90
|
+
----
|
91
|
+
Lutaml::Model::FormatRegistry.register(
|
92
|
+
:pairs,
|
93
|
+
mapping_class: PairsMapping,
|
94
|
+
adapter_class: PairsAdapter,
|
95
|
+
transformer: PairsTransform
|
96
|
+
)
|
97
|
+
----
|
98
|
+
|
99
|
+
== Using Custom Adapters
|
100
|
+
|
101
|
+
Once registered, you can use your custom adapter with your model classes:
|
102
|
+
|
103
|
+
[source,ruby]
|
104
|
+
----
|
105
|
+
class Person < Lutaml::Model::Serializable
|
106
|
+
attribute :name, :string
|
107
|
+
attribute :age, :string
|
108
|
+
|
109
|
+
pairs do
|
110
|
+
map_field "name", to: :name
|
111
|
+
map_field "age", to: :age
|
112
|
+
end
|
113
|
+
end
|
114
|
+
----
|
115
|
+
|
116
|
+
=== Step-by-step Example
|
117
|
+
|
118
|
+
[source,ruby]
|
119
|
+
----
|
120
|
+
input = "name:John|age:30"
|
121
|
+
|
122
|
+
person = Person.from_pairs(input)
|
123
|
+
# => #<Person @name="John", @age="30">
|
124
|
+
|
125
|
+
output = person.to_pairs_format
|
126
|
+
# => "name:John|age:30"
|
127
|
+
----
|
128
|
+
|
129
|
+
== Best Practices
|
130
|
+
|
131
|
+
. *Separation of Concerns*: Keep parsing, mapping, and transformation logic in their respective classes.
|
132
|
+
. *Error Handling*: Add meaningful error handling in your adapter's `.parse` method.
|
133
|
+
. *Documentation*: Clearly document the format and adapter usage.
|
134
|
+
. *Testing*: Write tests for your adapter's logic and behavior.
|
135
|
+
. *Leverage Format Features*: Support format-specific options while staying consistent with Lutaml::Model’s interfaces.
|
136
|
+
|
137
|
+
== Example Implementations
|
138
|
+
|
139
|
+
For complete examples of custom adapter implementations, see:
|
140
|
+
|
141
|
+
* `spec/lutaml/model/custom_bibtex_adapter_spec.rb`
|
142
|
+
* `spec/lutaml/model/custom_vobject_adapter_spec.rb`
|
143
|
+
|
144
|
+
These demonstrate how to build complete, custom adapters that integrate cleanly with `Lutaml::Model`.
|
@@ -14,6 +14,9 @@ module Lutaml
|
|
14
14
|
choice
|
15
15
|
sequence
|
16
16
|
method_name
|
17
|
+
polymorphic
|
18
|
+
polymorphic_class
|
19
|
+
initialize_empty
|
17
20
|
].freeze
|
18
21
|
|
19
22
|
def initialize(name, type, options = {})
|
@@ -25,6 +28,10 @@ module Lutaml
|
|
25
28
|
process_options!
|
26
29
|
end
|
27
30
|
|
31
|
+
def polymorphic?
|
32
|
+
@options[:polymorphic_class]
|
33
|
+
end
|
34
|
+
|
28
35
|
def derived?
|
29
36
|
type.nil?
|
30
37
|
end
|
@@ -41,6 +48,10 @@ module Lutaml
|
|
41
48
|
@options[:method_name]
|
42
49
|
end
|
43
50
|
|
51
|
+
def initialize_empty?
|
52
|
+
@options[:initialize_empty]
|
53
|
+
end
|
54
|
+
|
44
55
|
def cast_type!(type)
|
45
56
|
case type
|
46
57
|
when Symbol
|
@@ -97,11 +108,17 @@ module Lutaml
|
|
97
108
|
type.attributes[to].default
|
98
109
|
elsif options[:default].is_a?(Proc)
|
99
110
|
options[:default].call
|
100
|
-
|
111
|
+
elsif options.key?(:default)
|
101
112
|
options[:default]
|
113
|
+
else
|
114
|
+
Lutaml::Model::UninitializedClass.instance
|
102
115
|
end
|
103
116
|
end
|
104
117
|
|
118
|
+
def default_set?
|
119
|
+
!Utils.uninitialized?(default_value)
|
120
|
+
end
|
121
|
+
|
105
122
|
def pattern
|
106
123
|
options[:pattern]
|
107
124
|
end
|
@@ -121,6 +138,7 @@ module Lutaml
|
|
121
138
|
def valid_value!(value)
|
122
139
|
return true if value.nil? && singular?
|
123
140
|
return true unless enum?
|
141
|
+
return true if Utils.uninitialized?(value)
|
124
142
|
|
125
143
|
unless valid_value?(value)
|
126
144
|
raise Lutaml::Model::InvalidValueError.new(name, value, enum_values)
|
@@ -162,7 +180,21 @@ module Lutaml
|
|
162
180
|
|
163
181
|
valid_value!(value) &&
|
164
182
|
valid_collection!(value, self) &&
|
165
|
-
valid_pattern!(value)
|
183
|
+
valid_pattern!(value) &&
|
184
|
+
validate_polymorphic!(value)
|
185
|
+
end
|
186
|
+
|
187
|
+
def validate_polymorphic(value)
|
188
|
+
return value.all? { |v| validate_polymorphic!(v) } if value.is_a?(Array)
|
189
|
+
return true unless options[:polymorphic]
|
190
|
+
|
191
|
+
valid_polymorphic_type?(value)
|
192
|
+
end
|
193
|
+
|
194
|
+
def validate_polymorphic!(value)
|
195
|
+
return true if validate_polymorphic(value)
|
196
|
+
|
197
|
+
raise Lutaml::Model::PolymorphicError.new(value, options, type)
|
166
198
|
end
|
167
199
|
|
168
200
|
def validate_collection_range
|
@@ -200,9 +232,6 @@ module Lutaml
|
|
200
232
|
|
201
233
|
return true unless collection?
|
202
234
|
|
203
|
-
# Allow nil values for collections during initialization
|
204
|
-
return true if value.nil?
|
205
|
-
|
206
235
|
# Allow any value for unbounded collections
|
207
236
|
return true if options[:collection] == true
|
208
237
|
|
@@ -235,7 +264,8 @@ module Lutaml
|
|
235
264
|
end
|
236
265
|
|
237
266
|
def serialize(value, format, options = {})
|
238
|
-
|
267
|
+
value ||= [] if collection? && initialize_empty?
|
268
|
+
return value if value.nil? || Utils.uninitialized?(value)
|
239
269
|
return value if derived?
|
240
270
|
return serialize_array(value, format, options) if value.is_a?(Array)
|
241
271
|
return serialize_model(value, format, options) if type <= Serialize
|
@@ -244,25 +274,66 @@ module Lutaml
|
|
244
274
|
end
|
245
275
|
|
246
276
|
def cast(value, format, options = {})
|
247
|
-
|
248
|
-
|
249
|
-
value ||= [] if collection?
|
277
|
+
value ||= [] if collection? && !value.nil?
|
250
278
|
return value.map { |v| cast(v, format, options) } if value.is_a?(Array)
|
251
279
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
280
|
+
return value if already_serialized?(type, value)
|
281
|
+
|
282
|
+
klass = resolve_polymorphic_class(type, value, options)
|
283
|
+
|
284
|
+
if can_serialize?(klass, value, format)
|
285
|
+
klass.apply_mappings(value, format, options)
|
286
|
+
elsif needs_conversion?(klass, value)
|
287
|
+
klass.send(:"from_#{format}", value)
|
256
288
|
else
|
257
|
-
|
289
|
+
klass.cast(value)
|
258
290
|
end
|
259
291
|
end
|
260
292
|
|
293
|
+
def serializable?
|
294
|
+
type <= Serialize
|
295
|
+
end
|
296
|
+
|
297
|
+
def deep_dup
|
298
|
+
self.class.new(name, type, Utils.deep_dup(options))
|
299
|
+
end
|
300
|
+
|
261
301
|
private
|
262
302
|
|
303
|
+
def resolve_polymorphic_class(type, value, options)
|
304
|
+
return type unless polymorphic_map_defined?(options, value)
|
305
|
+
|
306
|
+
val = value[options[:polymorphic][:attribute]]
|
307
|
+
klass_name = options[:polymorphic][:class_map][val]
|
308
|
+
Object.const_get(klass_name)
|
309
|
+
end
|
310
|
+
|
311
|
+
def polymorphic_map_defined?(polymorphic_options, value)
|
312
|
+
!value.nil? &&
|
313
|
+
polymorphic_options[:polymorphic] &&
|
314
|
+
!polymorphic_options[:polymorphic].empty? &&
|
315
|
+
value[polymorphic_options[:polymorphic][:attribute]]
|
316
|
+
end
|
317
|
+
|
263
318
|
def castable?(value, format)
|
264
319
|
value.is_a?(Hash) ||
|
265
|
-
(format == :xml && value.is_a?(Lutaml::Model::
|
320
|
+
(format == :xml && value.is_a?(Lutaml::Model::Xml::XmlElement))
|
321
|
+
end
|
322
|
+
|
323
|
+
def castable_serialized_type?(value)
|
324
|
+
type <= Serialize && value.is_a?(type.model)
|
325
|
+
end
|
326
|
+
|
327
|
+
def can_serialize?(klass, value, format)
|
328
|
+
klass <= Serialize && castable?(value, format)
|
329
|
+
end
|
330
|
+
|
331
|
+
def needs_conversion?(klass, value)
|
332
|
+
!value.nil? && !value.is_a?(klass)
|
333
|
+
end
|
334
|
+
|
335
|
+
def already_serialized?(klass, value)
|
336
|
+
klass <= Serialize && value.is_a?(klass.model)
|
266
337
|
end
|
267
338
|
|
268
339
|
def serialize_array(value, format, options)
|
@@ -300,7 +371,7 @@ module Lutaml
|
|
300
371
|
|
301
372
|
def set_default_for_collection
|
302
373
|
validate_collection_range
|
303
|
-
@options[:default] ||= -> { [] }
|
374
|
+
@options[:default] ||= -> { [] } if initialize_empty?
|
304
375
|
end
|
305
376
|
|
306
377
|
def validate_options!(options)
|
@@ -315,6 +386,10 @@ module Lutaml
|
|
315
386
|
"`pattern` is only allowed for :string type"
|
316
387
|
end
|
317
388
|
|
389
|
+
if initialize_empty? && !collection?
|
390
|
+
raise StandardError,
|
391
|
+
"Invalid option `initialize_empty` given without `collection: true` option"
|
392
|
+
end
|
318
393
|
true
|
319
394
|
end
|
320
395
|
|
@@ -325,6 +400,16 @@ module Lutaml
|
|
325
400
|
raise ArgumentError,
|
326
401
|
"Invalid type: #{type}, must be a Symbol, String or a Class"
|
327
402
|
end
|
403
|
+
|
404
|
+
def valid_polymorphic_type?(value)
|
405
|
+
return value.is_a?(type) unless has_polymorphic_list?
|
406
|
+
|
407
|
+
options[:polymorphic].include?(value.class) && value.is_a?(type)
|
408
|
+
end
|
409
|
+
|
410
|
+
def has_polymorphic_list?
|
411
|
+
options[:polymorphic]&.is_a?(Array)
|
412
|
+
end
|
328
413
|
end
|
329
414
|
end
|
330
415
|
end
|
data/lib/lutaml/model/choice.rb
CHANGED
@@ -15,6 +15,13 @@ module Lutaml
|
|
15
15
|
raise Lutaml::Model::InvalidChoiceRangeError.new(@min, @max) if @min.negative? || @max.negative?
|
16
16
|
end
|
17
17
|
|
18
|
+
def ==(other)
|
19
|
+
@attributes == other.attributes &&
|
20
|
+
@min == other.min &&
|
21
|
+
@max == other.max &&
|
22
|
+
@model == other.model
|
23
|
+
end
|
24
|
+
|
18
25
|
def attribute(name, type, options = {})
|
19
26
|
options[:choice] = self
|
20
27
|
@attributes << @model.attribute(name, type, options)
|