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.
Files changed (148) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos-todo.json +7 -0
  3. data/.github/workflows/dependent-repos.json +17 -9
  4. data/.rubocop.yml +1 -1
  5. data/.rubocop_todo.yml +51 -65
  6. data/Gemfile +4 -1
  7. data/README.adoc +5083 -2612
  8. data/RELEASE_NOTES.adoc +346 -0
  9. data/docs/custom_adapters.adoc +144 -0
  10. data/lib/lutaml/model/attribute.rb +101 -16
  11. data/lib/lutaml/model/choice.rb +7 -0
  12. data/lib/lutaml/model/comparable_model.rb +48 -9
  13. data/lib/lutaml/model/config.rb +48 -42
  14. data/lib/lutaml/model/error/collection_count_out_of_range_error.rb +1 -1
  15. data/lib/lutaml/model/error/polymorphic_error.rb +14 -0
  16. data/lib/lutaml/model/error.rb +1 -0
  17. data/lib/lutaml/model/format_registry.rb +41 -0
  18. data/lib/lutaml/model/hash/document.rb +11 -0
  19. data/lib/lutaml/model/hash/mapping.rb +19 -0
  20. data/lib/lutaml/model/hash/mapping_rule.rb +9 -0
  21. data/lib/lutaml/model/hash/standard_adapter.rb +17 -0
  22. data/lib/lutaml/model/hash/transform.rb +8 -0
  23. data/lib/lutaml/model/hash.rb +21 -0
  24. data/lib/lutaml/model/json/document.rb +11 -0
  25. data/lib/lutaml/model/json/mapping.rb +19 -0
  26. data/lib/lutaml/model/json/mapping_rule.rb +9 -0
  27. data/lib/lutaml/model/{json_adapter → json}/multi_json_adapter.rb +4 -5
  28. data/lib/lutaml/model/{json_adapter/standard_json_adapter.rb → json/standard_adapter.rb} +5 -3
  29. data/lib/lutaml/model/json/transform.rb +8 -0
  30. data/lib/lutaml/model/json.rb +21 -0
  31. data/lib/lutaml/model/key_value_document.rb +27 -0
  32. data/lib/lutaml/model/{key_value_mapping.rb → mapping/key_value_mapping.rb} +64 -16
  33. data/lib/lutaml/model/{key_value_mapping_rule.rb → mapping/key_value_mapping_rule.rb} +18 -2
  34. data/lib/lutaml/model/mapping/mapping.rb +13 -0
  35. data/lib/lutaml/model/mapping/mapping_rule.rb +300 -0
  36. data/lib/lutaml/model/schema/xml_compiler.rb +15 -15
  37. data/lib/lutaml/model/sequence.rb +2 -2
  38. data/lib/lutaml/model/serialization_adapter.rb +22 -0
  39. data/lib/lutaml/model/serialize.rb +219 -444
  40. data/lib/lutaml/model/services/logger.rb +54 -0
  41. data/lib/lutaml/model/services/transformer.rb +48 -0
  42. data/lib/lutaml/model/services.rb +2 -0
  43. data/lib/lutaml/model/toml/document.rb +11 -0
  44. data/lib/lutaml/model/toml/mapping.rb +27 -0
  45. data/lib/lutaml/model/toml/mapping_rule.rb +9 -0
  46. data/lib/lutaml/model/{toml_adapter → toml}/toml_rb_adapter.rb +3 -3
  47. data/lib/lutaml/model/toml/tomlib_adapter.rb +19 -0
  48. data/lib/lutaml/model/toml/transform.rb +8 -0
  49. data/lib/lutaml/model/toml.rb +30 -0
  50. data/lib/lutaml/model/transform/key_value_transform.rb +291 -0
  51. data/lib/lutaml/model/transform/xml_transform.rb +239 -0
  52. data/lib/lutaml/model/transform.rb +78 -0
  53. data/lib/lutaml/model/type/date.rb +1 -1
  54. data/lib/lutaml/model/type/date_time.rb +2 -2
  55. data/lib/lutaml/model/type/hash.rb +1 -1
  56. data/lib/lutaml/model/type/time.rb +2 -2
  57. data/lib/lutaml/model/type/time_without_date.rb +2 -2
  58. data/lib/lutaml/model/type/value.rb +6 -9
  59. data/lib/lutaml/model/uninitialized_class.rb +64 -0
  60. data/lib/lutaml/model/utils.rb +44 -0
  61. data/lib/lutaml/model/validation.rb +1 -0
  62. data/lib/lutaml/model/version.rb +1 -1
  63. data/lib/lutaml/model/{xml_adapter → xml}/builder/nokogiri.rb +2 -2
  64. data/lib/lutaml/model/{xml_adapter → xml}/builder/oga.rb +10 -10
  65. data/lib/lutaml/model/{xml_adapter → xml}/builder/ox.rb +1 -1
  66. data/lib/lutaml/model/{xml_adapter/xml_document.rb → xml/document.rb} +41 -21
  67. data/lib/lutaml/model/xml/element.rb +32 -0
  68. data/lib/lutaml/model/xml/mapping.rb +410 -0
  69. data/lib/lutaml/model/xml/mapping_rule.rb +141 -0
  70. data/lib/lutaml/model/xml/nokogiri_adapter.rb +232 -0
  71. data/lib/lutaml/model/{xml_adapter → xml}/oga/document.rb +1 -1
  72. data/lib/lutaml/model/{xml_adapter → xml}/oga/element.rb +3 -1
  73. data/lib/lutaml/model/xml/oga_adapter.rb +171 -0
  74. data/lib/lutaml/model/xml/ox_adapter.rb +215 -0
  75. data/lib/lutaml/model/xml/transform.rb +8 -0
  76. data/lib/lutaml/model/{xml_adapter → xml}/xml_attribute.rb +1 -1
  77. data/lib/lutaml/model/{xml_adapter → xml}/xml_element.rb +23 -10
  78. data/lib/lutaml/model/{xml_adapter → xml}/xml_namespace.rb +1 -1
  79. data/lib/lutaml/model/xml.rb +31 -0
  80. data/lib/lutaml/model/xml_adapter/element.rb +11 -25
  81. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +6 -223
  82. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +13 -163
  83. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +10 -207
  84. data/lib/lutaml/model/yaml/document.rb +10 -0
  85. data/lib/lutaml/model/yaml/mapping.rb +19 -0
  86. data/lib/lutaml/model/yaml/mapping_rule.rb +9 -0
  87. data/lib/lutaml/model/{yaml_adapter/standard_yaml_adapter.rb → yaml/standard_adapter.rb} +4 -3
  88. data/lib/lutaml/model/yaml/transform.rb +8 -0
  89. data/lib/lutaml/model/yaml.rb +21 -0
  90. data/lib/lutaml/model.rb +40 -4
  91. data/lutaml-model.gemspec +0 -4
  92. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +7 -7
  93. data/spec/fixtures/person.rb +5 -5
  94. data/spec/lutaml/model/attribute_spec.rb +37 -1
  95. data/spec/lutaml/model/cdata_spec.rb +9 -9
  96. data/spec/lutaml/model/collection_spec.rb +50 -2
  97. data/spec/lutaml/model/comparable_model_spec.rb +92 -27
  98. data/spec/lutaml/model/custom_bibtex_adapter_spec.rb +598 -0
  99. data/spec/lutaml/model/custom_vobject_adapter_spec.rb +1226 -0
  100. data/spec/lutaml/model/defaults_spec.rb +1 -1
  101. data/spec/lutaml/model/enum_spec.rb +1 -1
  102. data/spec/lutaml/model/group_spec.rb +333 -20
  103. data/spec/lutaml/model/hash/adapter_spec.rb +255 -0
  104. data/spec/lutaml/model/json_adapter_spec.rb +6 -6
  105. data/spec/lutaml/model/key_value_mapping_spec.rb +65 -3
  106. data/spec/lutaml/model/mixed_content_spec.rb +24 -24
  107. data/spec/lutaml/model/multiple_mapping_spec.rb +5 -5
  108. data/spec/lutaml/model/ordered_content_spec.rb +6 -6
  109. data/spec/lutaml/model/polymorphic_spec.rb +526 -0
  110. data/spec/lutaml/model/render_empty_spec.rb +194 -0
  111. data/spec/lutaml/model/render_nil_spec.rb +206 -22
  112. data/spec/lutaml/model/root_mappings_spec.rb +3 -3
  113. data/spec/lutaml/model/schema/xml_compiler_spec.rb +6 -6
  114. data/spec/lutaml/model/serializable_spec.rb +179 -103
  115. data/spec/lutaml/model/simple_model_spec.rb +9 -9
  116. data/spec/lutaml/model/toml_adapter_spec.rb +6 -6
  117. data/spec/lutaml/model/toml_spec.rb +51 -0
  118. data/spec/lutaml/model/transformation_spec.rb +72 -15
  119. data/spec/lutaml/model/uninitialized_class_spec.rb +96 -0
  120. data/spec/lutaml/model/value_map_spec.rb +240 -0
  121. data/spec/lutaml/model/xml/namespace/nested_with_explicit_namespace_spec.rb +85 -0
  122. data/spec/lutaml/model/xml/namespace_spec.rb +57 -0
  123. data/spec/lutaml/model/xml/xml_element_spec.rb +93 -0
  124. data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +2 -2
  125. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +2 -2
  126. data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +2 -2
  127. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +6 -6
  128. data/spec/lutaml/model/xml_adapter_spec.rb +6 -6
  129. data/spec/lutaml/model/xml_mapping_rule_spec.rb +105 -5
  130. data/spec/lutaml/model/xml_mapping_spec.rb +70 -16
  131. data/spec/lutaml/model/xml_spec.rb +63 -0
  132. data/spec/lutaml/model/yaml_adapter_spec.rb +3 -5
  133. data/spec/sample_model_spec.rb +3 -3
  134. data/spec/spec_helper.rb +3 -3
  135. metadata +76 -59
  136. data/lib/lutaml/model/json_adapter/json_document.rb +0 -20
  137. data/lib/lutaml/model/json_adapter/json_object.rb +0 -28
  138. data/lib/lutaml/model/loggable.rb +0 -15
  139. data/lib/lutaml/model/mapping_rule.rb +0 -109
  140. data/lib/lutaml/model/toml_adapter/toml_document.rb +0 -20
  141. data/lib/lutaml/model/toml_adapter/toml_object.rb +0 -28
  142. data/lib/lutaml/model/toml_adapter/tomlib_adapter.rb +0 -20
  143. data/lib/lutaml/model/toml_adapter.rb +0 -6
  144. data/lib/lutaml/model/xml_mapping.rb +0 -307
  145. data/lib/lutaml/model/xml_mapping_rule.rb +0 -122
  146. data/lib/lutaml/model/yaml_adapter/yaml_document.rb +0 -20
  147. data/lib/lutaml/model/yaml_adapter/yaml_object.rb +0 -28
  148. data/lib/lutaml/model/yaml_adapter.rb +0 -8
@@ -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
- else
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
- return if value.nil?
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
- return value if type <= Serialize && value.is_a?(type.model)
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
- if type <= Serialize && castable?(value, format)
253
- type.apply_mappings(value, format, options)
254
- elsif !value.nil? && !value.is_a?(type)
255
- type.send(:"from_#{format}", value)
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
- type.cast(value)
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::XmlAdapter::XmlElement))
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
@@ -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)