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.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.rubocop_todo.yml +49 -48
  4. data/Gemfile +4 -1
  5. data/README.adoc +791 -143
  6. data/RELEASE_NOTES.adoc +346 -0
  7. data/docs/custom_adapters.adoc +144 -0
  8. data/lib/lutaml/model/attribute.rb +17 -11
  9. data/lib/lutaml/model/config.rb +48 -42
  10. data/lib/lutaml/model/error/polymorphic_error.rb +7 -2
  11. data/lib/lutaml/model/format_registry.rb +41 -0
  12. data/lib/lutaml/model/hash/document.rb +11 -0
  13. data/lib/lutaml/model/hash/mapping.rb +19 -0
  14. data/lib/lutaml/model/hash/mapping_rule.rb +9 -0
  15. data/lib/lutaml/model/hash/standard_adapter.rb +17 -0
  16. data/lib/lutaml/model/hash/transform.rb +8 -0
  17. data/lib/lutaml/model/hash.rb +21 -0
  18. data/lib/lutaml/model/json/document.rb +11 -0
  19. data/lib/lutaml/model/json/mapping.rb +19 -0
  20. data/lib/lutaml/model/json/mapping_rule.rb +9 -0
  21. data/lib/lutaml/model/{json_adapter → json}/multi_json_adapter.rb +4 -5
  22. data/lib/lutaml/model/{json_adapter/standard_json_adapter.rb → json/standard_adapter.rb} +5 -3
  23. data/lib/lutaml/model/json/transform.rb +8 -0
  24. data/lib/lutaml/model/json.rb +21 -0
  25. data/lib/lutaml/model/key_value_document.rb +27 -0
  26. data/lib/lutaml/model/mapping/key_value_mapping.rb +8 -4
  27. data/lib/lutaml/model/mapping/mapping.rb +13 -0
  28. data/lib/lutaml/model/mapping/mapping_rule.rb +7 -6
  29. data/lib/lutaml/model/serialization_adapter.rb +22 -0
  30. data/lib/lutaml/model/serialize.rb +146 -521
  31. data/lib/lutaml/model/services/logger.rb +54 -0
  32. data/lib/lutaml/model/services/transformer.rb +48 -0
  33. data/lib/lutaml/model/services.rb +2 -0
  34. data/lib/lutaml/model/toml/document.rb +11 -0
  35. data/lib/lutaml/model/toml/mapping.rb +27 -0
  36. data/lib/lutaml/model/toml/mapping_rule.rb +9 -0
  37. data/lib/lutaml/model/{toml_adapter → toml}/toml_rb_adapter.rb +3 -3
  38. data/lib/lutaml/model/toml/tomlib_adapter.rb +19 -0
  39. data/lib/lutaml/model/toml/transform.rb +8 -0
  40. data/lib/lutaml/model/toml.rb +30 -0
  41. data/lib/lutaml/model/transform/key_value_transform.rb +291 -0
  42. data/lib/lutaml/model/transform/xml_transform.rb +239 -0
  43. data/lib/lutaml/model/transform.rb +78 -0
  44. data/lib/lutaml/model/type/value.rb +6 -9
  45. data/lib/lutaml/model/uninitialized_class.rb +1 -1
  46. data/lib/lutaml/model/utils.rb +30 -0
  47. data/lib/lutaml/model/version.rb +1 -1
  48. data/lib/lutaml/model/{xml_adapter → xml}/builder/nokogiri.rb +2 -2
  49. data/lib/lutaml/model/{xml_adapter → xml}/builder/oga.rb +10 -10
  50. data/lib/lutaml/model/{xml_adapter → xml}/builder/ox.rb +1 -1
  51. data/lib/lutaml/model/{xml_adapter/xml_document.rb → xml/document.rb} +6 -7
  52. data/lib/lutaml/model/xml/element.rb +32 -0
  53. data/lib/lutaml/model/xml/mapping.rb +410 -0
  54. data/lib/lutaml/model/xml/mapping_rule.rb +141 -0
  55. data/lib/lutaml/model/xml/nokogiri_adapter.rb +232 -0
  56. data/lib/lutaml/model/{xml_adapter → xml}/oga/document.rb +1 -1
  57. data/lib/lutaml/model/{xml_adapter → xml}/oga/element.rb +3 -1
  58. data/lib/lutaml/model/xml/oga_adapter.rb +171 -0
  59. data/lib/lutaml/model/xml/ox_adapter.rb +215 -0
  60. data/lib/lutaml/model/xml/transform.rb +8 -0
  61. data/lib/lutaml/model/{xml_adapter → xml}/xml_attribute.rb +1 -1
  62. data/lib/lutaml/model/{xml_adapter → xml}/xml_element.rb +6 -3
  63. data/lib/lutaml/model/{xml_adapter → xml}/xml_namespace.rb +1 -1
  64. data/lib/lutaml/model/xml.rb +31 -0
  65. data/lib/lutaml/model/xml_adapter/element.rb +11 -25
  66. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +6 -223
  67. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +13 -163
  68. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +10 -207
  69. data/lib/lutaml/model/yaml/document.rb +10 -0
  70. data/lib/lutaml/model/yaml/mapping.rb +19 -0
  71. data/lib/lutaml/model/yaml/mapping_rule.rb +9 -0
  72. data/lib/lutaml/model/{yaml_adapter/standard_yaml_adapter.rb → yaml/standard_adapter.rb} +4 -3
  73. data/lib/lutaml/model/yaml/transform.rb +8 -0
  74. data/lib/lutaml/model/yaml.rb +21 -0
  75. data/lib/lutaml/model.rb +39 -4
  76. data/lutaml-model.gemspec +0 -4
  77. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +4 -4
  78. data/spec/lutaml/model/cdata_spec.rb +7 -7
  79. data/spec/lutaml/model/custom_bibtex_adapter_spec.rb +598 -0
  80. data/spec/lutaml/model/custom_vobject_adapter_spec.rb +1226 -0
  81. data/spec/lutaml/model/group_spec.rb +18 -7
  82. data/spec/lutaml/model/hash/adapter_spec.rb +255 -0
  83. data/spec/lutaml/model/json_adapter_spec.rb +6 -6
  84. data/spec/lutaml/model/key_value_mapping_spec.rb +25 -1
  85. data/spec/lutaml/model/mixed_content_spec.rb +24 -24
  86. data/spec/lutaml/model/multiple_mapping_spec.rb +5 -5
  87. data/spec/lutaml/model/ordered_content_spec.rb +6 -6
  88. data/spec/lutaml/model/polymorphic_spec.rb +178 -0
  89. data/spec/lutaml/model/root_mappings_spec.rb +3 -3
  90. data/spec/lutaml/model/schema/xml_compiler_spec.rb +6 -6
  91. data/spec/lutaml/model/serializable_spec.rb +179 -103
  92. data/spec/lutaml/model/toml_adapter_spec.rb +6 -6
  93. data/spec/lutaml/model/toml_spec.rb +51 -0
  94. data/spec/lutaml/model/transformation_spec.rb +72 -15
  95. data/spec/lutaml/model/uninitialized_class_spec.rb +96 -0
  96. data/spec/lutaml/model/xml/namespace_spec.rb +57 -0
  97. data/spec/lutaml/model/xml/xml_element_spec.rb +1 -1
  98. data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +2 -2
  99. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +2 -2
  100. data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +2 -2
  101. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +6 -6
  102. data/spec/lutaml/model/xml_adapter_spec.rb +6 -6
  103. data/spec/lutaml/model/xml_mapping_rule_spec.rb +3 -3
  104. data/spec/lutaml/model/xml_mapping_spec.rb +26 -14
  105. data/spec/lutaml/model/xml_spec.rb +63 -0
  106. data/spec/lutaml/model/yaml_adapter_spec.rb +3 -5
  107. data/spec/spec_helper.rb +3 -3
  108. metadata +64 -59
  109. data/lib/lutaml/model/json_adapter/json_document.rb +0 -20
  110. data/lib/lutaml/model/json_adapter/json_object.rb +0 -28
  111. data/lib/lutaml/model/loggable.rb +0 -15
  112. data/lib/lutaml/model/mapping/json_mapping.rb +0 -17
  113. data/lib/lutaml/model/mapping/toml_mapping.rb +0 -25
  114. data/lib/lutaml/model/mapping/xml_mapping.rb +0 -389
  115. data/lib/lutaml/model/mapping/xml_mapping_rule.rb +0 -139
  116. data/lib/lutaml/model/mapping/yaml_mapping.rb +0 -17
  117. data/lib/lutaml/model/mapping.rb +0 -14
  118. data/lib/lutaml/model/toml_adapter/toml_document.rb +0 -20
  119. data/lib/lutaml/model/toml_adapter/toml_object.rb +0 -28
  120. data/lib/lutaml/model/toml_adapter/tomlib_adapter.rb +0 -20
  121. data/lib/lutaml/model/toml_adapter.rb +0 -6
  122. data/lib/lutaml/model/yaml_adapter/yaml_document.rb +0 -20
  123. data/lib/lutaml/model/yaml_adapter/yaml_object.rb +0 -28
  124. 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`.
@@ -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 polymorphic_enabled?
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?(options, value)
311
+ def polymorphic_map_defined?(polymorphic_options, value)
308
312
  !value.nil? &&
309
- options[:polymorphic] &&
310
- !options[:polymorphic].empty? &&
311
- value[options[:polymorphic][:attribute]]
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::XmlAdapter::XmlElement))
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 polymorphic_enabled?
401
- options[:polymorphic]&.any?
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 valid_polymorphic_type?(value)
405
- value.is_a?(type) && options[:polymorphic].include?(value.class)
410
+ def has_polymorphic_list?
411
+ options[:polymorphic]&.is_a?(Array)
406
412
  end
407
413
  end
408
414
  end
@@ -3,12 +3,7 @@ module Lutaml
3
3
  module Config
4
4
  extend self
5
5
 
6
- # Default values are set for these so the readers are defined below
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 = "#{adapter_name}_adapter"
48
- type = "#{type_name}_adapter"
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
- begin
51
- adapter_file = File.join(adapter, type)
52
- require_relative adapter_file
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
- instance_variable_set(
65
- :"@#{adapter}",
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
- # Return JSON adapter. By default StandardJsonAdapter is used
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
- # Return YAML adapter. By default StandardYamlAdapter is used
82
- #
83
- # @example
84
- # Lutaml::Model::Config.yaml_adapter
85
- # # => Lutaml::Model::YamlAdapter::StandardYamlAdapter
86
- def yaml_adapter
87
- @yaml_adapter || Lutaml::Model::YamlAdapter::StandardYamlAdapter
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
- super("#{value.class} not in #{options[:polymorphic]}")
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