lutaml-model 0.7.1 → 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/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +49 -48
- data/Gemfile +4 -1
- data/README.adoc +791 -143
- data/RELEASE_NOTES.adoc +346 -0
- data/docs/custom_adapters.adoc +144 -0
- data/lib/lutaml/model/attribute.rb +17 -11
- data/lib/lutaml/model/config.rb +48 -42
- data/lib/lutaml/model/error/polymorphic_error.rb +7 -2
- 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/mapping/key_value_mapping.rb +8 -4
- data/lib/lutaml/model/mapping/mapping.rb +13 -0
- data/lib/lutaml/model/mapping/mapping_rule.rb +7 -6
- data/lib/lutaml/model/serialization_adapter.rb +22 -0
- data/lib/lutaml/model/serialize.rb +146 -521
- 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/value.rb +6 -9
- data/lib/lutaml/model/uninitialized_class.rb +1 -1
- data/lib/lutaml/model/utils.rb +30 -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} +6 -7
- 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 +6 -3
- 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 +39 -4
- data/lutaml-model.gemspec +0 -4
- data/spec/benchmarks/xml_parsing_benchmark_spec.rb +4 -4
- data/spec/lutaml/model/cdata_spec.rb +7 -7
- 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/group_spec.rb +18 -7
- 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 +25 -1
- 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 +178 -0
- 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/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/xml/namespace_spec.rb +57 -0
- data/spec/lutaml/model/xml/xml_element_spec.rb +1 -1
- 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 +3 -3
- data/spec/lutaml/model/xml_mapping_spec.rb +26 -14
- data/spec/lutaml/model/xml_spec.rb +63 -0
- data/spec/lutaml/model/yaml_adapter_spec.rb +3 -5
- data/spec/spec_helper.rb +3 -3
- metadata +64 -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/json_mapping.rb +0 -17
- data/lib/lutaml/model/mapping/toml_mapping.rb +0 -25
- data/lib/lutaml/model/mapping/xml_mapping.rb +0 -389
- data/lib/lutaml/model/mapping/xml_mapping_rule.rb +0 -139
- data/lib/lutaml/model/mapping/yaml_mapping.rb +0 -17
- data/lib/lutaml/model/mapping.rb +0 -14
- 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/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`.
|
@@ -186,7 +186,7 @@ module Lutaml
|
|
186
186
|
|
187
187
|
def validate_polymorphic(value)
|
188
188
|
return value.all? { |v| validate_polymorphic!(v) } if value.is_a?(Array)
|
189
|
-
return true unless
|
189
|
+
return true unless options[:polymorphic]
|
190
190
|
|
191
191
|
valid_polymorphic_type?(value)
|
192
192
|
end
|
@@ -194,7 +194,7 @@ module Lutaml
|
|
194
194
|
def validate_polymorphic!(value)
|
195
195
|
return true if validate_polymorphic(value)
|
196
196
|
|
197
|
-
raise Lutaml::Model::PolymorphicError.new(value, options)
|
197
|
+
raise Lutaml::Model::PolymorphicError.new(value, options, type)
|
198
198
|
end
|
199
199
|
|
200
200
|
def validate_collection_range
|
@@ -290,6 +290,10 @@ module Lutaml
|
|
290
290
|
end
|
291
291
|
end
|
292
292
|
|
293
|
+
def serializable?
|
294
|
+
type <= Serialize
|
295
|
+
end
|
296
|
+
|
293
297
|
def deep_dup
|
294
298
|
self.class.new(name, type, Utils.deep_dup(options))
|
295
299
|
end
|
@@ -304,16 +308,16 @@ module Lutaml
|
|
304
308
|
Object.const_get(klass_name)
|
305
309
|
end
|
306
310
|
|
307
|
-
def polymorphic_map_defined?(
|
311
|
+
def polymorphic_map_defined?(polymorphic_options, value)
|
308
312
|
!value.nil? &&
|
309
|
-
|
310
|
-
!
|
311
|
-
value[
|
313
|
+
polymorphic_options[:polymorphic] &&
|
314
|
+
!polymorphic_options[:polymorphic].empty? &&
|
315
|
+
value[polymorphic_options[:polymorphic][:attribute]]
|
312
316
|
end
|
313
317
|
|
314
318
|
def castable?(value, format)
|
315
319
|
value.is_a?(Hash) ||
|
316
|
-
(format == :xml && value.is_a?(Lutaml::Model::
|
320
|
+
(format == :xml && value.is_a?(Lutaml::Model::Xml::XmlElement))
|
317
321
|
end
|
318
322
|
|
319
323
|
def castable_serialized_type?(value)
|
@@ -397,12 +401,14 @@ module Lutaml
|
|
397
401
|
"Invalid type: #{type}, must be a Symbol, String or a Class"
|
398
402
|
end
|
399
403
|
|
400
|
-
def
|
401
|
-
|
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)
|
402
408
|
end
|
403
409
|
|
404
|
-
def
|
405
|
-
|
410
|
+
def has_polymorphic_list?
|
411
|
+
options[:polymorphic]&.is_a?(Array)
|
406
412
|
end
|
407
413
|
end
|
408
414
|
end
|
data/lib/lutaml/model/config.rb
CHANGED
@@ -3,12 +3,7 @@ module Lutaml
|
|
3
3
|
module Config
|
4
4
|
extend self
|
5
5
|
|
6
|
-
|
7
|
-
attr_writer :json_adapter, :yaml_adapter
|
8
|
-
|
9
|
-
attr_accessor :xml_adapter, :toml_adapter
|
10
|
-
|
11
|
-
AVAILABLE_FORMATS = %i[xml json yaml toml].freeze
|
6
|
+
AVAILABLE_FORMATS = %i[xml json yaml toml hash].freeze
|
12
7
|
KEY_VALUE_FORMATS = AVAILABLE_FORMATS - %i[xml]
|
13
8
|
|
14
9
|
def configure
|
@@ -42,55 +37,66 @@ module Lutaml
|
|
42
37
|
# one of [:tomlib, :toml_rb]
|
43
38
|
# @example
|
44
39
|
# Lutaml::Model::Config.toml_adapter = :tomlib
|
40
|
+
#
|
41
|
+
# AVAILABLE_FORMATS.each do |adapter_name|
|
42
|
+
# define_method(:"#{adapter_name}_adapter_type=") do |type_name|
|
43
|
+
# Lutaml::Model::FormatRegistry.send(:"#{adapter_name}_adapter_type=", type_name)
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
|
45
47
|
AVAILABLE_FORMATS.each do |adapter_name|
|
46
48
|
define_method(:"#{adapter_name}_adapter_type=") do |type_name|
|
47
|
-
adapter =
|
48
|
-
type =
|
49
|
+
adapter = adapter_name.to_s
|
50
|
+
type = normalize_type_name(type_name, adapter_name)
|
51
|
+
load_adapter_file(adapter, type)
|
52
|
+
load_moxml_adapter(type_name, adapter_name)
|
53
|
+
set_adapter_for(adapter_name, class_for(adapter, type))
|
54
|
+
end
|
55
|
+
end
|
49
56
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
rescue LoadError
|
54
|
-
raise(
|
55
|
-
Lutaml::Model::UnknownAdapterTypeError.new(
|
56
|
-
adapter_name,
|
57
|
-
type_name,
|
58
|
-
),
|
59
|
-
cause: nil,
|
60
|
-
)
|
61
|
-
end
|
62
|
-
Moxml::Adapter.load(type_name) unless KEY_VALUE_FORMATS.include?(adapter_name)
|
57
|
+
def adapter_for(format)
|
58
|
+
public_send(:"#{format}_adapter")
|
59
|
+
end
|
63
60
|
|
64
|
-
|
65
|
-
|
66
|
-
Lutaml::Model.const_get(to_class_name(adapter))
|
67
|
-
.const_get(to_class_name(type)),
|
68
|
-
)
|
69
|
-
end
|
61
|
+
def set_adapter_for(format, adapter)
|
62
|
+
public_send(:"#{format}_adapter=", adapter)
|
70
63
|
end
|
71
64
|
|
72
|
-
|
73
|
-
|
74
|
-
# @example
|
75
|
-
# Lutaml::Model::Config.json_adapter
|
76
|
-
# # => Lutaml::Model::YamlAdapter::StandardJsonAdapter
|
77
|
-
def json_adapter
|
78
|
-
@json_adapter || Lutaml::Model::JsonAdapter::StandardJsonAdapter
|
65
|
+
def mappings_class_for(format)
|
66
|
+
Lutaml::Model::FormatRegistry.mappings_class_for(format)
|
79
67
|
end
|
80
68
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
69
|
+
def transformer_for(format)
|
70
|
+
Lutaml::Model::FormatRegistry.transformer_for(format)
|
71
|
+
end
|
72
|
+
|
73
|
+
def class_for(adapter, type)
|
74
|
+
Lutaml::Model.const_get(to_class_name(adapter))
|
75
|
+
.const_get(to_class_name(type))
|
88
76
|
end
|
89
77
|
|
90
|
-
# @api private
|
91
78
|
def to_class_name(str)
|
92
79
|
str.to_s.split("_").map(&:capitalize).join
|
93
80
|
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def normalize_type_name(type_name, adapter_name)
|
85
|
+
"#{type_name.to_s.gsub("_#{adapter_name}", '')}_adapter"
|
86
|
+
end
|
87
|
+
|
88
|
+
def load_adapter_file(adapter, type)
|
89
|
+
adapter_file = File.join(adapter, type)
|
90
|
+
require_relative adapter_file
|
91
|
+
rescue LoadError
|
92
|
+
raise UnknownAdapterTypeError.new(adapter, type), cause: nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def load_moxml_adapter(type_name, adapter_name)
|
96
|
+
return if KEY_VALUE_FORMATS.include?(adapter_name)
|
97
|
+
|
98
|
+
Moxml::Adapter.load(type_name)
|
99
|
+
end
|
94
100
|
end
|
95
101
|
end
|
96
102
|
end
|
@@ -1,8 +1,13 @@
|
|
1
1
|
module Lutaml
|
2
2
|
module Model
|
3
3
|
class PolymorphicError < Error
|
4
|
-
def initialize(value, options)
|
5
|
-
|
4
|
+
def initialize(value, options, type)
|
5
|
+
error = if options[:polymorphic].is_a?(Array)
|
6
|
+
"#{value.class} not in #{options[:polymorphic]}"
|
7
|
+
else
|
8
|
+
"#{value.class} is not valid sub class of #{type}"
|
9
|
+
end
|
10
|
+
super(error)
|
6
11
|
end
|
7
12
|
end
|
8
13
|
end
|