lutaml-model 0.7.1 → 0.7.7

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 (259) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/.github/workflows/dependent-tests.yml +4 -0
  4. data/.github/workflows/rake.yml +12 -0
  5. data/.github/workflows/release.yml +3 -0
  6. data/.gitignore +6 -1
  7. data/.irbrc +1 -0
  8. data/.pryrc +1 -0
  9. data/.rubocop.yml +1 -1
  10. data/.rubocop_todo.yml +58 -84
  11. data/Gemfile +4 -1
  12. data/README.adoc +3115 -365
  13. data/RELEASE_NOTES.adoc +346 -0
  14. data/docs/custom_adapters.adoc +144 -0
  15. data/docs/custom_registers.adoc +228 -0
  16. data/docs/schema_generation.adoc +898 -0
  17. data/docs/schema_import.adoc +364 -0
  18. data/flake.lock +114 -0
  19. data/flake.nix +103 -0
  20. data/lib/lutaml/model/attribute.rb +242 -100
  21. data/lib/lutaml/model/choice.rb +30 -0
  22. data/lib/lutaml/model/collection.rb +212 -0
  23. data/lib/lutaml/model/comparable_model.rb +3 -3
  24. data/lib/lutaml/model/config.rb +71 -42
  25. data/lib/lutaml/model/constants.rb +2 -0
  26. data/lib/lutaml/model/error/element_count_out_of_range_error.rb +29 -0
  27. data/lib/lutaml/model/error/invalid_attribute_name_error.rb +15 -0
  28. data/lib/lutaml/model/error/invalid_attribute_options_error.rb +16 -0
  29. data/lib/lutaml/model/error/invalid_choice_range_error.rb +3 -5
  30. data/lib/lutaml/model/error/polymorphic_error.rb +7 -2
  31. data/lib/lutaml/model/error/register/not_registrable_class_error.rb +11 -0
  32. data/lib/lutaml/model/error/type/invalid_value_error.rb +5 -3
  33. data/lib/lutaml/model/error/type/max_bound_error.rb +20 -0
  34. data/lib/lutaml/model/error/type/max_length_error.rb +20 -0
  35. data/lib/lutaml/model/error/type/min_bound_error.rb +20 -0
  36. data/lib/lutaml/model/error/type/min_length_error.rb +20 -0
  37. data/lib/lutaml/model/error/type/pattern_not_matched_error.rb +18 -0
  38. data/lib/lutaml/model/error/validation_failed_error.rb +9 -0
  39. data/lib/lutaml/model/error.rb +10 -0
  40. data/lib/lutaml/model/errors.rb +36 -0
  41. data/lib/lutaml/model/format_registry.rb +44 -0
  42. data/lib/lutaml/model/global_register.rb +41 -0
  43. data/lib/lutaml/model/hash_adapter/document.rb +11 -0
  44. data/lib/lutaml/model/hash_adapter/mapping.rb +19 -0
  45. data/lib/lutaml/model/hash_adapter/mapping_rule.rb +9 -0
  46. data/lib/lutaml/model/hash_adapter/standard_adapter.rb +17 -0
  47. data/lib/lutaml/model/hash_adapter/transform.rb +8 -0
  48. data/lib/lutaml/model/hash_adapter.rb +21 -0
  49. data/lib/lutaml/model/json/document.rb +11 -0
  50. data/lib/lutaml/model/json/mapping.rb +19 -0
  51. data/lib/lutaml/model/json/mapping_rule.rb +9 -0
  52. data/lib/lutaml/model/{json_adapter → json}/multi_json_adapter.rb +4 -5
  53. data/lib/lutaml/model/{json_adapter/standard_json_adapter.rb → json/standard_adapter.rb} +5 -3
  54. data/lib/lutaml/model/json/transform.rb +8 -0
  55. data/lib/lutaml/model/json.rb +21 -0
  56. data/lib/lutaml/model/jsonl/document.rb +14 -0
  57. data/lib/lutaml/model/jsonl/mapping.rb +19 -0
  58. data/lib/lutaml/model/jsonl/mapping_rule.rb +9 -0
  59. data/lib/lutaml/model/jsonl/standard_adapter.rb +33 -0
  60. data/lib/lutaml/model/jsonl/transform.rb +19 -0
  61. data/lib/lutaml/model/jsonl.rb +21 -0
  62. data/lib/lutaml/model/key_value_document.rb +28 -0
  63. data/lib/lutaml/model/mapping/key_value_mapping.rb +85 -13
  64. data/lib/lutaml/model/mapping/key_value_mapping_rule.rb +5 -0
  65. data/lib/lutaml/model/mapping/mapping.rb +13 -0
  66. data/lib/lutaml/model/mapping/mapping_rule.rb +15 -9
  67. data/lib/lutaml/model/register.rb +105 -0
  68. data/lib/lutaml/model/{toml_adapter.rb → registrable.rb} +1 -1
  69. data/lib/lutaml/model/schema/base_schema.rb +64 -0
  70. data/lib/lutaml/model/schema/decorators/attribute.rb +114 -0
  71. data/lib/lutaml/model/schema/decorators/choices.rb +31 -0
  72. data/lib/lutaml/model/schema/decorators/class_definition.rb +85 -0
  73. data/lib/lutaml/model/schema/decorators/definition_collection.rb +97 -0
  74. data/lib/lutaml/model/schema/generator/definition.rb +53 -0
  75. data/lib/lutaml/model/schema/generator/definitions_collection.rb +81 -0
  76. data/lib/lutaml/model/schema/generator/properties_collection.rb +63 -0
  77. data/lib/lutaml/model/schema/generator/property.rb +110 -0
  78. data/lib/lutaml/model/schema/generator/ref.rb +24 -0
  79. data/lib/lutaml/model/schema/helpers/template_helper.rb +49 -0
  80. data/lib/lutaml/model/schema/json_schema.rb +42 -49
  81. data/lib/lutaml/model/schema/relaxng_schema.rb +14 -10
  82. data/lib/lutaml/model/schema/renderer.rb +36 -0
  83. data/lib/lutaml/model/schema/shared_methods.rb +24 -0
  84. data/lib/lutaml/model/schema/templates/model.erb +9 -0
  85. data/lib/lutaml/model/schema/xml_compiler/attribute.rb +85 -0
  86. data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +45 -0
  87. data/lib/lutaml/model/schema/xml_compiler/choice.rb +65 -0
  88. data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +27 -0
  89. data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +34 -0
  90. data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +136 -0
  91. data/lib/lutaml/model/schema/xml_compiler/element.rb +104 -0
  92. data/lib/lutaml/model/schema/xml_compiler/group.rb +97 -0
  93. data/lib/lutaml/model/schema/xml_compiler/restriction.rb +101 -0
  94. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +50 -0
  95. data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +36 -0
  96. data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +189 -0
  97. data/lib/lutaml/model/schema/xml_compiler.rb +231 -587
  98. data/lib/lutaml/model/schema/xsd_schema.rb +12 -8
  99. data/lib/lutaml/model/schema/yaml_schema.rb +41 -35
  100. data/lib/lutaml/model/schema.rb +1 -0
  101. data/lib/lutaml/model/sequence.rb +60 -30
  102. data/lib/lutaml/model/serialization_adapter.rb +22 -0
  103. data/lib/lutaml/model/serialize.rb +296 -548
  104. data/lib/lutaml/model/services/base.rb +11 -0
  105. data/lib/lutaml/model/services/logger.rb +54 -0
  106. data/lib/lutaml/model/services/rule_value_extractor.rb +92 -0
  107. data/lib/lutaml/model/services/transformer.rb +48 -0
  108. data/lib/lutaml/model/services/type/validator/number.rb +25 -0
  109. data/lib/lutaml/model/services/type/validator/string.rb +52 -0
  110. data/lib/lutaml/model/services/type/validator.rb +43 -0
  111. data/lib/lutaml/model/services/validator.rb +145 -0
  112. data/lib/lutaml/model/services.rb +5 -0
  113. data/lib/lutaml/model/toml/document.rb +11 -0
  114. data/lib/lutaml/model/toml/mapping.rb +27 -0
  115. data/lib/lutaml/model/toml/mapping_rule.rb +9 -0
  116. data/lib/lutaml/model/{toml_adapter → toml}/toml_rb_adapter.rb +3 -3
  117. data/lib/lutaml/model/toml/tomlib_adapter.rb +19 -0
  118. data/lib/lutaml/model/toml/transform.rb +8 -0
  119. data/lib/lutaml/model/toml.rb +30 -0
  120. data/lib/lutaml/model/transform/key_value_transform.rb +297 -0
  121. data/lib/lutaml/model/transform/xml_transform.rb +228 -0
  122. data/lib/lutaml/model/transform.rb +93 -0
  123. data/lib/lutaml/model/type/boolean.rb +1 -1
  124. data/lib/lutaml/model/type/date.rb +1 -1
  125. data/lib/lutaml/model/type/date_time.rb +1 -1
  126. data/lib/lutaml/model/type/decimal.rb +11 -9
  127. data/lib/lutaml/model/type/float.rb +2 -1
  128. data/lib/lutaml/model/type/integer.rb +24 -21
  129. data/lib/lutaml/model/type/string.rb +4 -2
  130. data/lib/lutaml/model/type/time.rb +1 -1
  131. data/lib/lutaml/model/type/time_without_date.rb +1 -1
  132. data/lib/lutaml/model/type/value.rb +11 -10
  133. data/lib/lutaml/model/type.rb +5 -2
  134. data/lib/lutaml/model/uninitialized_class.rb +1 -1
  135. data/lib/lutaml/model/utils.rb +56 -4
  136. data/lib/lutaml/model/validation.rb +6 -4
  137. data/lib/lutaml/model/version.rb +1 -1
  138. data/lib/lutaml/model/{xml_adapter → xml}/builder/nokogiri.rb +2 -2
  139. data/lib/lutaml/model/{xml_adapter → xml}/builder/oga.rb +10 -10
  140. data/lib/lutaml/model/{xml_adapter → xml}/builder/ox.rb +1 -1
  141. data/lib/lutaml/model/{xml_adapter/xml_document.rb → xml/document.rb} +42 -25
  142. data/lib/lutaml/model/xml/element.rb +32 -0
  143. data/lib/lutaml/model/xml/mapping.rb +471 -0
  144. data/lib/lutaml/model/xml/mapping_rule.rb +149 -0
  145. data/lib/lutaml/model/xml/nokogiri_adapter.rb +234 -0
  146. data/lib/lutaml/model/{xml_adapter → xml}/oga/document.rb +1 -1
  147. data/lib/lutaml/model/{xml_adapter → xml}/oga/element.rb +7 -2
  148. data/lib/lutaml/model/xml/oga_adapter.rb +172 -0
  149. data/lib/lutaml/model/xml/ox_adapter.rb +229 -0
  150. data/lib/lutaml/model/xml/transform.rb +8 -0
  151. data/lib/lutaml/model/{xml_adapter → xml}/xml_attribute.rb +1 -1
  152. data/lib/lutaml/model/{xml_adapter → xml}/xml_element.rb +9 -31
  153. data/lib/lutaml/model/{xml_adapter → xml}/xml_namespace.rb +1 -1
  154. data/lib/lutaml/model/xml.rb +31 -0
  155. data/lib/lutaml/model/xml_adapter/element.rb +11 -25
  156. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +6 -223
  157. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +13 -163
  158. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +10 -207
  159. data/lib/lutaml/model/yaml/document.rb +10 -0
  160. data/lib/lutaml/model/yaml/mapping.rb +19 -0
  161. data/lib/lutaml/model/yaml/mapping_rule.rb +9 -0
  162. data/lib/lutaml/model/{yaml_adapter/standard_yaml_adapter.rb → yaml/standard_adapter.rb} +4 -3
  163. data/lib/lutaml/model/yaml/transform.rb +8 -0
  164. data/lib/lutaml/model/yaml.rb +21 -0
  165. data/lib/lutaml/model/yamls/document.rb +14 -0
  166. data/lib/lutaml/model/yamls/mapping.rb +19 -0
  167. data/lib/lutaml/model/yamls/mapping_rule.rb +9 -0
  168. data/lib/lutaml/model/yamls/standard_adapter.rb +34 -0
  169. data/lib/lutaml/model/yamls/transform.rb +19 -0
  170. data/lib/lutaml/model/yamls.rb +21 -0
  171. data/lib/lutaml/model.rb +15 -4
  172. data/lutaml-model.gemspec +0 -4
  173. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +5 -6
  174. data/spec/fixtures/xml/advanced_test_schema.xsd +134 -0
  175. data/spec/fixtures/xml/examples/nested_categories.xml +55 -0
  176. data/spec/fixtures/xml/examples/valid_catalog.xml +43 -0
  177. data/spec/fixtures/xml/product_catalog.xsd +151 -0
  178. data/spec/fixtures/xml/specifications_schema.xsd +38 -0
  179. data/spec/lutaml/model/attribute_collection_spec.rb +101 -0
  180. data/spec/lutaml/model/attribute_spec.rb +41 -44
  181. data/spec/lutaml/model/cdata_spec.rb +7 -7
  182. data/spec/lutaml/model/choice_spec.rb +44 -0
  183. data/spec/lutaml/model/custom_bibtex_adapter_spec.rb +598 -0
  184. data/spec/lutaml/model/custom_collection_spec.rb +830 -0
  185. data/spec/lutaml/model/custom_vobject_adapter_spec.rb +1226 -0
  186. data/spec/lutaml/model/global_register_spec.rb +108 -0
  187. data/spec/lutaml/model/group_spec.rb +27 -10
  188. data/spec/lutaml/model/hash/adapter_spec.rb +255 -0
  189. data/spec/lutaml/model/json_adapter_spec.rb +6 -6
  190. data/spec/lutaml/model/jsonl/standard_adapter_spec.rb +91 -0
  191. data/spec/lutaml/model/jsonl_spec.rb +229 -0
  192. data/spec/lutaml/model/key_value_mapping_spec.rb +25 -1
  193. data/spec/lutaml/model/mixed_content_spec.rb +24 -24
  194. data/spec/lutaml/model/multiple_mapping_spec.rb +6 -6
  195. data/spec/lutaml/model/ordered_content_spec.rb +6 -6
  196. data/spec/lutaml/model/polymorphic_spec.rb +178 -0
  197. data/spec/lutaml/model/register/key_value_spec.rb +275 -0
  198. data/spec/lutaml/model/register/xml_spec.rb +185 -0
  199. data/spec/lutaml/model/register_spec.rb +147 -0
  200. data/spec/lutaml/model/root_mappings_spec.rb +3 -3
  201. data/spec/lutaml/model/rule_value_extractor_spec.rb +162 -0
  202. data/spec/lutaml/model/schema/generator/definitions_collection_spec.rb +120 -0
  203. data/spec/lutaml/model/schema/json_schema_spec.rb +412 -51
  204. data/spec/lutaml/model/schema/json_schema_to_models_spec.rb +383 -0
  205. data/spec/lutaml/model/schema/xml_compiler/attribute_group_spec.rb +65 -0
  206. data/spec/lutaml/model/schema/xml_compiler/attribute_spec.rb +63 -0
  207. data/spec/lutaml/model/schema/xml_compiler/choice_spec.rb +71 -0
  208. data/spec/lutaml/model/schema/xml_compiler/complex_content_restriction_spec.rb +55 -0
  209. data/spec/lutaml/model/schema/xml_compiler/complex_content_spec.rb +37 -0
  210. data/spec/lutaml/model/schema/xml_compiler/complex_type_spec.rb +173 -0
  211. data/spec/lutaml/model/schema/xml_compiler/element_spec.rb +63 -0
  212. data/spec/lutaml/model/schema/xml_compiler/group_spec.rb +86 -0
  213. data/spec/lutaml/model/schema/xml_compiler/restriction_spec.rb +76 -0
  214. data/spec/lutaml/model/schema/xml_compiler/sequence_spec.rb +59 -0
  215. data/spec/lutaml/model/schema/xml_compiler/simple_content_spec.rb +55 -0
  216. data/spec/lutaml/model/schema/xml_compiler/simple_type_spec.rb +181 -0
  217. data/spec/lutaml/model/schema/xml_compiler_spec.rb +503 -1804
  218. data/spec/lutaml/model/schema/yaml_schema_spec.rb +249 -26
  219. data/spec/lutaml/model/sequence_spec.rb +36 -0
  220. data/spec/lutaml/model/serializable_spec.rb +201 -94
  221. data/spec/lutaml/model/toml_adapter_spec.rb +6 -6
  222. data/spec/lutaml/model/toml_spec.rb +51 -0
  223. data/spec/lutaml/model/transformation_spec.rb +72 -15
  224. data/spec/lutaml/model/type_spec.rb +8 -4
  225. data/spec/lutaml/model/uninitialized_class_spec.rb +96 -0
  226. data/spec/lutaml/model/utils_spec.rb +3 -3
  227. data/spec/lutaml/model/xml/derived_attributes_spec.rb +1 -1
  228. data/spec/lutaml/model/xml/namespace_spec.rb +57 -0
  229. data/spec/lutaml/model/xml/root_mappings/nested_child_mappings_spec.rb +217 -0
  230. data/spec/lutaml/model/xml/xml_element_spec.rb +7 -1
  231. data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +2 -2
  232. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +2 -2
  233. data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +2 -2
  234. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +12 -12
  235. data/spec/lutaml/model/xml_adapter_spec.rb +30 -6
  236. data/spec/lutaml/model/xml_mapping_rule_spec.rb +14 -7
  237. data/spec/lutaml/model/xml_mapping_spec.rb +27 -15
  238. data/spec/lutaml/model/xml_spec.rb +63 -0
  239. data/spec/lutaml/model/yaml_adapter_spec.rb +3 -5
  240. data/spec/lutaml/model/yamls/standard_adapter_spec.rb +183 -0
  241. data/spec/lutaml/model/yamls_spec.rb +294 -0
  242. data/spec/spec_helper.rb +4 -3
  243. metadata +162 -60
  244. data/lib/lutaml/model/json_adapter/json_document.rb +0 -20
  245. data/lib/lutaml/model/json_adapter/json_object.rb +0 -28
  246. data/lib/lutaml/model/loggable.rb +0 -15
  247. data/lib/lutaml/model/mapping/json_mapping.rb +0 -17
  248. data/lib/lutaml/model/mapping/toml_mapping.rb +0 -25
  249. data/lib/lutaml/model/mapping/xml_mapping.rb +0 -389
  250. data/lib/lutaml/model/mapping/xml_mapping_rule.rb +0 -139
  251. data/lib/lutaml/model/mapping/yaml_mapping.rb +0 -17
  252. data/lib/lutaml/model/mapping.rb +0 -14
  253. data/lib/lutaml/model/schema/templates/simple_type.rb +0 -247
  254. data/lib/lutaml/model/toml_adapter/toml_document.rb +0 -20
  255. data/lib/lutaml/model/toml_adapter/toml_object.rb +0 -28
  256. data/lib/lutaml/model/toml_adapter/tomlib_adapter.rb +0 -20
  257. data/lib/lutaml/model/yaml_adapter/yaml_document.rb +0 -20
  258. data/lib/lutaml/model/yaml_adapter/yaml_object.rb +0 -28
  259. data/lib/lutaml/model/yaml_adapter.rb +0 -8
data/README.adoc CHANGED
@@ -17,7 +17,7 @@ for:
17
17
 
18
18
  It provides simple, flexible and comprehensive mechanisms for defining
19
19
  information models with attributes and types, and the serialization of them
20
- to/from serialization formats including JSON, XML, YAML, and TOML.
20
+ to/from serialization formats including Hash, JSON, XML, YAML, and TOML.
21
21
 
22
22
  For serialization formats, it uses an adapter pattern to support multiple
23
23
  libraries for each format, providing flexibility and extensibility for your data
@@ -33,12 +33,17 @@ Lutaml::Model are provided in <<migrate-from-shale>>.
33
33
  == Features
34
34
 
35
35
  * Define models with attributes and types
36
- * Serialize and deserialize models to/from JSON, XML, YAML, and TOML
36
+ * Serialize and deserialize models to/from Hash, JSON, XML, YAML, and TOML
37
37
  * Support for multiple serialization libraries (e.g., `toml-rb`, `tomlib`)
38
38
  * Configurable adapters for different serialization formats
39
39
  * Support for collections and default values
40
40
  * Custom serialization/deserialization methods
41
41
  * XML namespaces and mappings
42
+ * Generate serialization schemas from model definitions (<<schema-generation>>)
43
+ * Import serialization schemas to define models (<<schema-import>>)
44
+ * Create custom adapters for additional data formats (see <<custom-adapters>>)
45
+ * Dynamically modify model attribute types using registers (see <<custom-registers>>)
46
+
42
47
 
43
48
 
44
49
  == Data modeling in a nutshell
@@ -269,10 +274,26 @@ Or install it yourself as:
269
274
  gem install lutaml-model
270
275
  ----
271
276
 
277
+
278
+ == Components
279
+
280
+ LutaML provides the following set of components to model information in a
281
+ structured way.
282
+
283
+ * <<model-definition,Basic models>>
284
+ * <<attribute-definition,Attributes in models>>
285
+ * <<value-definition,Values assigned to attributes>>
286
+ * <<collection-definition,Collections of models>>
287
+
288
+
289
+ [[model-definition]]
272
290
  == Model
273
291
 
274
292
  === General
275
293
 
294
+ A LutaML model is used to represent a class of information, of which a model
295
+ instance is a set of information representing a coherent concept.
296
+
276
297
  There are two ways to define an information model in Lutaml::Model:
277
298
 
278
299
  * Inheriting from the `Lutaml::Model::Serializable` class
@@ -376,7 +397,7 @@ same class and all their attributes are equal.
376
397
  > # true
377
398
  ----
378
399
 
379
-
400
+ [[value-definition]]
380
401
  == Value types
381
402
 
382
403
  === General types
@@ -496,7 +517,7 @@ serialization format and returns the object to be assigned to the `Value` class'
496
517
  `to_{format}`:: Serializes the object to a string of the serialization format.
497
518
 
498
519
  The `{format}` part of the method name is the serialization format in lowercase
499
- (e.g. `json`, `xml`, `yaml`, `toml`).
520
+ (e.g. `hash`, `json`, `xml`, `yaml`, `toml`).
500
521
 
501
522
  .Using custom serialization methods to handle a high-precision date-time type
502
523
  [example]
@@ -564,6 +585,7 @@ part lost due to the inability of JSON to handle high-precision date-time.
564
585
  ====
565
586
 
566
587
 
588
+ [[attribute-definition]]
567
589
  == Attributes
568
590
 
569
591
 
@@ -615,6 +637,56 @@ puts s.established
615
637
  ----
616
638
  ====
617
639
 
640
+ ==== Restricting the value of an attribute
641
+
642
+ The `restrict` class method is used to update or refine the validation rules for an attribute that has already been defined. This allows you to apply additional or stricter constraints to an existing attribute without redefining it.
643
+
644
+ .Using the `restrict` class method to update the options of an existing attribute
645
+ [example]
646
+ ====
647
+ [source,ruby]
648
+ ----
649
+ class Studio < Lutaml::Model::Serializable
650
+ attribute :name, :string
651
+ restrict :name, collection: 1..3, pattern: /[A-Za-z]+/
652
+ end
653
+ ----
654
+ ====
655
+
656
+ .Apply different restrictions to the existing attribute in multiple subclasses
657
+ [example]
658
+ ====
659
+ [source,ruby]
660
+ ----
661
+ class Document < Lutaml::Model::Serializable
662
+ attribute :status, :string
663
+ end
664
+
665
+ class DraftDocument < Document
666
+ # Only allow "draft" or "in_review" as valid statuses for drafts
667
+ restrict :status, values: %w[draft in_review]
668
+ end
669
+
670
+ class PublishedDocument < Document
671
+ # Only allow "published" or "archived" as valid statuses for published documents
672
+ restrict :status, values: %w[published archived]
673
+ end
674
+
675
+ # Usage
676
+ # Call .validate! to trigger validation and raise an error if the value is not allowed
677
+ Document.new(status: "draft").validate! # valid, there are no validation rules for `Document`
678
+ Document.new(status: "published").validate! # valid, there are no validation rules for `Document`
679
+ DraftDocument.new(status: "draft").validate! # valid
680
+ DraftDocument.new(status: "in_review").validate! # valid
681
+ DraftDocument.new(status: "published").validate! # raises error (not allowed)
682
+ PublishedDocument.new(status: "published").validate! # valid
683
+ PublishedDocument.new(status: "archived").validate! # valid
684
+ PublishedDocument.new(status: "draft").validate! # raises error (not allowed)
685
+ ----
686
+ ====
687
+
688
+ All options that are supported by the `attribute` class method are also supported by the `restrict` method. Any unsupported option passed to `restrict` will result in a `Lutaml::Model::InvalidAttributeOptionsError` being raised.
689
+
618
690
  === Polymorphic attributes
619
691
 
620
692
  ==== General
@@ -820,7 +892,7 @@ end
820
892
  <1> The name of the polymorphic attribute.
821
893
  <2> The polymorphic superclass class.
822
894
  <3> Any options for the attribute.
823
- <4> The `polymorphic` option that determines the acceptable polymorphic subclasses.
895
+ <4> The `polymorphic` option that determines the acceptable polymorphic subclasses, or just `true`.
824
896
  <5> The polymorphic subclasses.
825
897
 
826
898
  The `polymorphic` option is an array of polymorphic subclasses that the
@@ -863,6 +935,25 @@ end
863
935
  ----
864
936
  ====
865
937
 
938
+ * If the attribute (collection or not) is meant to contain instances belonging
939
+ to any polymorphic subclass of a defined base class, then set the `polymorphic:
940
+ true` option.
941
+ +
942
+ [example]
943
+ ====
944
+ In the following code, `ReferenceSet` is a class that has a polymorphic
945
+ attribute `references`. The `references` attribute can accept instances of
946
+ any polymorphic subclass of the `Reference` base class, so `polymorphic: true`
947
+ is set.
948
+
949
+ [source,ruby]
950
+ ----
951
+ class ReferenceSet < Lutaml::Model::Serializable
952
+ attribute :references, Reference, collection: true, polymorphic: true
953
+ end
954
+ ----
955
+ ====
956
+
866
957
  * If the attribute (collection or not) is meant to contain instances belonging
867
958
  to more than one polymorphic subclass, then those acceptable polymorphic
868
959
  subclasses should be explicitly specified in the `polymorphic: [...]` option.
@@ -1051,16 +1142,16 @@ class Reference < Lutaml::Model::Serializable
1051
1142
 
1052
1143
  xml do
1053
1144
  map_attribute "reference-type", to: :_class, polymorphic_map: {
1054
- "document-ref" => "PolymorphicSpec::Base::DocumentReference",
1055
- "anchor-ref" => "PolymorphicSpec::Base::AnchorReference",
1145
+ "document-ref" => "DocumentReference",
1146
+ "anchor-ref" => "AnchorReference",
1056
1147
  }
1057
1148
  map_element "name", to: :name
1058
1149
  end
1059
1150
 
1060
1151
  key_value do
1061
1152
  map "_class", to: :_class, polymorphic_map: {
1062
- "Document" => "PolymorphicSpec::Base::DocumentReference",
1063
- "Anchor" => "PolymorphicSpec::Base::AnchorReference",
1153
+ "Document" => "DocumentReference",
1154
+ "Anchor" => "AnchorReference",
1064
1155
  }
1065
1156
  map "name", to: :name
1066
1157
  end
@@ -1484,9 +1575,13 @@ puts SomeModel.new.to_yaml
1484
1575
  ----
1485
1576
  ====
1486
1577
 
1578
+
1487
1579
  === Derived attributes
1488
1580
 
1489
- A derived attribute is computed dynamically based on an instance method instead of storing a static value. It is defined using the `method:` option.
1581
+ A derived attribute has a value computed dynamically on evaluation of an
1582
+ instance method.
1583
+
1584
+ It is defined using the `method:` option.
1490
1585
 
1491
1586
  Syntax:
1492
1587
 
@@ -1572,6 +1667,7 @@ attributes.
1572
1667
  * The last attribute `completeName` is optional.
1573
1668
  ====
1574
1669
 
1670
+ NOTE: The `choice` directive can be used with `import_model_attributes`. For more details, see <<import-model-attributes-inside-choice, Using import_model_attributes inside a choice block>>.
1575
1671
 
1576
1672
 
1577
1673
  === Importable models for reuse
@@ -1628,6 +1724,7 @@ class ComplexType < Lutaml::Model::Serializable
1628
1724
 
1629
1725
  xml do
1630
1726
  root "GroupOfItems"
1727
+
1631
1728
  map_attribute "tag", to: :tag
1632
1729
  map_content to: :content
1633
1730
  map_element :group, to: :group
@@ -1659,6 +1756,161 @@ end
1659
1756
  ----
1660
1757
  ====
1661
1758
 
1759
+ [[import-model-mappings-inside-sequence]]
1760
+ ==== Using `import_model_mappings` inside a `sequence`
1761
+
1762
+ You can use `import_model_mappings` within a `sequence` block to include the element mappings from another model. This is useful for composing complex XML structures from reusable model components.
1763
+
1764
+ The element mappings will be imported inside this specific `sequence` block that calls the import method, rest of the mappings like `content`, `attributes`, etc. will be inserted at the class level.
1765
+
1766
+ NOTE: `import_model` and `import_model_attributes` are not supported inside a `sequence` block.
1767
+
1768
+ [example]
1769
+ ====
1770
+ [source,ruby]
1771
+ ----
1772
+ class Address < Lutaml::Model::Serializable
1773
+ attribute :street, :string
1774
+ attribute :city, :string
1775
+ attribute :zip, :string
1776
+
1777
+ xml do
1778
+ no_root
1779
+
1780
+ map_element :street, to: :street
1781
+ map_element :city, to: :city
1782
+ map_element :zip, to: :zip
1783
+ end
1784
+ end
1785
+
1786
+ class Person < Lutaml::Model::Serializable
1787
+ attribute :name, :string
1788
+ import_model_attributes Address
1789
+
1790
+ xml do
1791
+ root "Person"
1792
+
1793
+ map_element :name, to: :name
1794
+ sequence do
1795
+ import_model_mappings Address
1796
+ end
1797
+ end
1798
+ end
1799
+
1800
+ # Example XML output:
1801
+ valid_xml = <<~XML
1802
+ <Person>
1803
+ <name>John Doe</name>
1804
+ <street>123 Main St</street>
1805
+ <city>Metropolis</city>
1806
+ <zip>12345</zip>
1807
+ </Person>
1808
+ XML
1809
+ invalid_xml = <<~XML
1810
+ <Person>
1811
+ <name>John Doe</name>
1812
+ <street>123 Main St</street>
1813
+ <zip>12345</zip>
1814
+ </Person>
1815
+ XML
1816
+ Person.from_xml(valid_xml) # #<Person:0x00000002d56b3988 @city="Metropolis", @name="John Doe", @street="123 Main St", @zip="12345">
1817
+ Person.from_xml(invalid_xml) # raises `Element `zip` does not match the expected sequence order element `city` (Lutaml::Model::IncorrectSequenceError)`
1818
+ ----
1819
+ ====
1820
+
1821
+ [[import-model-attributes-inside-choice]]
1822
+ ==== Using import_model_attributes inside a choice block
1823
+
1824
+ You can use `import_model_attributes` within a `choice` block to allow a model to accept one or more sets of attributes from other models, with flexible cardinality. This is especially useful when you want to allow a user to provide one or more alternative forms of information (e.g., contact methods) in your model.
1825
+
1826
+ For example, suppose you want a `Person` model that can have either an `email`, a `phone`, or both as contact information. You can define `ContactEmail` and `ContactPhone` as importable models, and then use `import_model_attributes` for both, inside a `choice` block in the `Person` model.
1827
+
1828
+ NOTE: The `import_model_attributes` method is used to import the attributes from the other model into the current model. The imported attributes will be associated to the `choice` block that calls the import method.
1829
+
1830
+ [example]
1831
+ ====
1832
+ [source,ruby]
1833
+ ----
1834
+ class ContactEmail < Lutaml::Model::Serializable
1835
+ attribute :email, :string
1836
+
1837
+ xml do
1838
+ no_root
1839
+
1840
+ map_element :email, to: :email
1841
+ end
1842
+ end
1843
+
1844
+ class ContactPhone < Lutaml::Model::Serializable
1845
+ attribute :phone, :string
1846
+
1847
+ xml do
1848
+ no_root
1849
+
1850
+ map_element :phone, to: :phone
1851
+ end
1852
+ end
1853
+
1854
+ class Person < Lutaml::Model::Serializable
1855
+ # Allow either or both contact methods, but at least one must be present
1856
+ choice(min: 1, max: 2) do
1857
+ import_model_attributes ContactEmail
1858
+ import_model_attributes ContactPhone
1859
+ end
1860
+
1861
+ xml do
1862
+ root "Person"
1863
+
1864
+ map_element :email, to: :email
1865
+ map_element :phone, to: :phone
1866
+ end
1867
+ end
1868
+
1869
+ valid_xml = <<~XML
1870
+ <Person>
1871
+ <email>john.doe@example.com</email>
1872
+ <phone>1234567890</phone>
1873
+ </Person>
1874
+ XML
1875
+
1876
+ Person.from_xml(valid_xml).validate! # #<Person:0x00000002d0e27fe8 @email="john.doe@example.com", @phone="1234567890">
1877
+
1878
+ invalid_xml = <<~XML
1879
+ <Person></Person>
1880
+ XML
1881
+
1882
+ Person.from_xml(invalid_xml).validate! # raises `Lutaml::Model::ValidationError` error
1883
+ ----
1884
+ ====
1885
+
1886
+ ==== Using register functionality
1887
+
1888
+ The register functionality is useful when you want to reference or reuse a model by a symbolic name (e.g., across files or in dynamic scenarios), rather than by direct class reference.
1889
+
1890
+ .Importing a model using a `Register`
1891
+ [example]
1892
+ ====
1893
+ [source,ruby]
1894
+ ----
1895
+ register = Lutaml::Model::Register.new(:importable_model)
1896
+ register.register_model(GroupOfItems, id: :group_of_items)
1897
+ ----
1898
+
1899
+ The `id: :group_of_items` assigns a symbolic name to the registered model, which can then be used in `import_model :group_of_items`.
1900
+
1901
+ [source,ruby]
1902
+ ----
1903
+ class GroupOfSubItems < Lutaml::Model::Serializable
1904
+ import_model :group_of_items
1905
+ end
1906
+ ----
1907
+ ====
1908
+
1909
+ The `import_model :group_of_items` will behave the same as `import_model GroupOfItems` except the class is resolved from the provided `register`.
1910
+
1911
+ NOTE: All the `import_*` methods support the use of `register` functionality.
1912
+
1913
+ NOTE: For more details on registers, see <<custom_registers, Custom Registers>>.
1662
1914
 
1663
1915
  [[attribute-value-transform]]
1664
1916
  === Attribute value transform
@@ -1974,156 +2226,1474 @@ For the following XML snippet:
1974
2226
  ----
1975
2227
  ====
1976
2228
 
1977
- == Serialization model mappings
2229
+
2230
+ == Collections
1978
2231
 
1979
2232
  === General
1980
2233
 
1981
- Lutaml::Model allows you to translate a data model into serialization models of
1982
- various serialization formats including XML, JSON, YAML, and TOML.
2234
+ Collections are used to represent a contained group of multiple instances of models.
1983
2235
 
1984
- Depending on the serialization format, different methods are supported for
1985
- defining serialization and deserialization mappings.
2236
+ Typically, a collection represents an "Array" or a "Set" in information modeling
2237
+ and programming languages. In LutaML, a collection represents an array of model
2238
+ instances.
1986
2239
 
1987
- Serialization model mappings are defined under the `xml`, `json`, `yaml`, and
1988
- `toml` blocks.
2240
+ Models in a collection may be:
1989
2241
 
1990
- .Using the `xml`, `json`, `yaml`, `toml` and `key_value` blocks to define serialization mappings
1991
- [source,ruby]
1992
- ----
1993
- class Example < Lutaml::Model::Serializable
1994
- xml do
1995
- # ...
1996
- end
2242
+ * constrained to be of a single kind;
1997
2243
 
1998
- json do
1999
- # ...
2000
- end
2244
+ * constrained to be of multiple kinds sharing common characteristics;
2001
2245
 
2002
- yaml do
2003
- # ...
2004
- end
2246
+ * unbounded of any kind.
2005
2247
 
2006
- toml do
2007
- # ...
2008
- end
2248
+ LutaML Model provides the `Lutaml::Model::Collection` class for defining
2249
+ collections of model instances.
2009
2250
 
2010
- key_value do
2011
- # ...
2012
- end
2251
+ === Configuration
2252
+
2253
+ ==== All formats
2254
+
2255
+ The `instances` directive defined at the `Collection` class level is used to
2256
+ define the collection attribute and the model type of the collection elements.
2257
+
2258
+ Syntax:
2259
+
2260
+ [source,ruby]
2261
+ ----
2262
+ class MyCollection < Lutaml::Model::Collection
2263
+ instances {attribute}, {ModelType}
2013
2264
  end
2014
2265
  ----
2015
2266
 
2267
+ Where,
2016
2268
 
2017
- === XML
2018
-
2019
- ==== Setting root element name
2269
+ `attribute`:: The name of the attribute that contains the collection.
2270
+ `ModelType`:: The model type of the collection elements.
2020
2271
 
2021
- The `root` method sets the root element tag name of the XML document.
2022
2272
 
2023
- If `root` is not given, then the snake-cased class name will be used as the
2024
- root.
2273
+ ==== Mapping instances: key-value formats only
2025
2274
 
2026
- [example]
2027
- Sets the tag name for `<example>` in XML `<example>...</example>`.
2275
+ The `map_instances` directive is only used in the `key_value` block.
2028
2276
 
2029
2277
  Syntax:
2030
2278
 
2031
2279
  [source,ruby]
2032
2280
  ----
2033
- xml do
2034
- root 'xml_element_name'
2035
- end
2036
- ----
2281
+ class MyCollection < Lutaml::Model::Collection
2282
+ instances {attribute}, ModelType
2037
2283
 
2038
- .Setting the root element name to `example`
2039
- [example]
2040
- ====
2041
- [source,ruby]
2042
- ----
2043
- class Example < Lutaml::Model::Serializable
2044
- xml do
2045
- root 'example'
2284
+ key_value do
2285
+ map_instances to: {attribute}
2046
2286
  end
2047
2287
  end
2048
2288
  ----
2049
2289
 
2050
- [source,ruby]
2051
- ----
2052
- > Example.new.to_xml
2053
- > #<example></example>
2054
- ----
2055
- ====
2290
+ Where,
2056
2291
 
2292
+ `attribute`:: The name of the attribute that contains model instances.
2057
2293
 
2058
- ==== Ommiting root element
2294
+ This directive maps individual array elements to the defined `instances`
2295
+ attribute. These are the items considered part of the Collection and reflected
2296
+ as Enumerable elements.
2059
2297
 
2060
- The root element can be omitted by using the `no_root` method.
2061
2298
 
2062
- When `no_root` is used, only `map_element` can be used because without a root
2063
- element there cannot be attributes.
2299
+ ==== Mapping instances: XML only
2064
2300
 
2065
- Syntax:
2301
+ In the `xml` block, the `map_element`, `map_attribute` directives are used instead.
2066
2302
 
2067
- [source,ruby]
2068
- ----
2069
- xml do
2070
- no_root
2071
- end
2072
- ----
2303
+ These directives map individual array elements to the defined `instances`
2304
+ attribute. These are the items considered part of the Collection and reflected
2305
+ as Enumerable elements.
2306
+
2307
+ Syntax for an element collection:
2073
2308
 
2074
- [example]
2075
- ====
2076
2309
  [source,ruby]
2077
2310
  ----
2078
- class NameAndCode < Lutaml::Model::Serializable
2079
- attribute :name, :string
2080
- attribute :code, :string
2311
+ class MyCollection < Lutaml::Model::Collection
2312
+ instances {attribute}, ModelType
2081
2313
 
2082
2314
  xml do
2083
- no_root
2084
- map_element "code", to: :code
2085
- map_element "name", to: :name
2315
+ map_element "element-name", to: {attribute}
2086
2316
  end
2087
2317
  end
2088
2318
  ----
2089
2319
 
2090
- [source,xml]
2091
- ----
2092
- <name>Name</name>
2093
- <code>ID-001</code>
2094
- ----
2320
+ Where,
2321
+
2322
+ `element-name`:: The name of the XML element of each model instance.
2323
+
2324
+
2325
+ Syntax for an attribute collection:
2095
2326
 
2096
2327
  [source,ruby]
2097
2328
  ----
2098
- > parsed = NameAndCode.from_xml(xml)
2099
- > # <NameAndCode:0x0000000107a3ca70 @code="ID-001", @name="Name">
2100
- > parsed.to_xml
2101
- > # <code>ID-001</code><name>Name</name>
2329
+ class MyCollection < Lutaml::Model::Collection
2330
+ instances {attribute}, ModelType
2331
+ xml do
2332
+ map_attribute "attribute-name", to: {attribute}
2333
+ end
2334
+ end
2102
2335
  ----
2103
- ====
2104
2336
 
2337
+ Where,
2105
2338
 
2339
+ `attribute-name`:: The name of the XML attribute that contains all model instances.
2106
2340
 
2107
- [[xml-map-all]]
2108
- ==== Mapping all XML content
2109
2341
 
2110
- The `map_all` tag in XML mapping captures and maps all content within an XML
2111
- element into a single attribute in the target Ruby object.
2342
+ === Collection types
2112
2343
 
2113
- The use case for `map_all` is to tell Lutaml::Model to not parse the content of
2114
- the XML element at all, and instead handle it as an XML string.
2344
+ A LutaML collections is used for a number of scenarios:
2115
2345
 
2116
- NOTE: The corresponding method for key-value formats is at <<key-value-map-all>>.
2346
+ * Root collections (for key-value formats)
2347
+ * Named collections
2348
+ * Keyed collections (for key-value formats)
2349
+ * Attribute collections
2350
+ * Nested collections
2351
+ // * Polymorphic collections
2352
+ // * Polymorphic attribute collections
2117
2353
 
2118
- WARNING: Notice that usage of mapping all will lead to incompatibility between
2119
- serialization formats, i.e. the raw string content will not be portable as
2120
- objects are across different formats.
2121
2354
 
2122
- This is useful in the case where the content of an XML element is not to be
2123
- handled by a Lutaml::Model::Serializable object.
2355
+ === Root collections (key-value formats only)
2124
2356
 
2125
- This feature is commonly used with custom methods or a custom model object to
2126
- handle the content.
2357
+ ==== General
2358
+
2359
+ TODO: This case needs to be fixed for JSON.
2360
+
2361
+ A root collection is a collection that is not contained within a parent
2362
+ collection.
2363
+
2364
+ Root collections only apply to key-value serialization formats.
2365
+ The XML format does not support root collections.
2366
+
2367
+ NOTE: The https://www.w3.org/TR/xml11/[XML standard] mandates the existence
2368
+ of a non-repeated "root element" in an XML document. This means that a valid XML
2369
+ document must have a root element, and all elements in an XML document must
2370
+ exist within the root. This is why an XML document cannot be a "root collection".
2371
+
2372
+ NOTE: A root collection cannot be represented using a non-collection model.
2373
+
2374
+ Root collections store multiple instances of the same model type at the root
2375
+ level. In other words, these are model instances that do not have a defined
2376
+ container at the level of the LutaML Model.
2377
+
2378
+ There are two kinds of root collections depending on the type of the instance
2379
+ value:
2380
+
2381
+ "Root value collection":: the value is a "primitive type"
2382
+
2383
+ "Root object collection":: the value is a "model instance"
2384
+
2385
+ Regardless of the type of root collection, the instance in a collection is
2386
+ always a LutaML model instance.
2387
+
2388
+
2389
+ ==== Root value collections
2390
+
2391
+ A root value collection is a collection that directly contains values of a
2392
+ primitive type.
2393
+
2394
+ .Simple root collection with each instance being a value
2395
+ [example]
2396
+ ====
2397
+ [source,yaml]
2398
+ ----
2399
+ ---
2400
+ - Item One
2401
+ - Item Two
2402
+ - Item Three
2403
+ ----
2404
+
2405
+ [source,json]
2406
+ ----
2407
+ [
2408
+ "Item One",
2409
+ "Item Two",
2410
+ "Item Three"
2411
+ ]
2412
+ ----
2413
+ ====
2414
+
2415
+
2416
+ Syntax:
2417
+
2418
+ [source,ruby]
2419
+ ----
2420
+ class MyCollection < Lutaml::Model::Collection
2421
+ instances :items, ModelType
2422
+ end
2423
+
2424
+ class ModelType < Lutaml::Model::Serializable
2425
+ attribute :name, :string
2426
+ end
2427
+ ----
2428
+
2429
+ .Handling a root collection where each instance is a value
2430
+ [example]
2431
+ ====
2432
+ Code:
2433
+
2434
+ [source,ruby]
2435
+ ----
2436
+ class Title < Lutaml::Model::Serializable
2437
+ attribute :content, :string
2438
+ end
2439
+
2440
+ class TitleCollection < Lutaml::Model::Collection
2441
+ instances :titles, Title
2442
+
2443
+ key_value do
2444
+ no_root # default
2445
+ map_instances to: :titles
2446
+ end
2447
+ end
2448
+ ----
2449
+
2450
+ Data:
2451
+
2452
+ [source,yaml]
2453
+ ----
2454
+ ---
2455
+ - Title One
2456
+ - Title Two
2457
+ - Title Three
2458
+ ----
2459
+
2460
+ [source,json]
2461
+ ----
2462
+ [
2463
+ "Title One",
2464
+ "Title Two",
2465
+ "Title Three"
2466
+ ]
2467
+ ----
2468
+
2469
+ Usage:
2470
+
2471
+ [source,ruby]
2472
+ ----
2473
+ titles = TitleCollection.from_yaml(yaml_data)
2474
+ titles.count
2475
+ # => 3
2476
+ titles.first.content
2477
+ # => "Title One"
2478
+ ----
2479
+ ====
2480
+
2481
+
2482
+ ==== Root object collections
2483
+
2484
+ A root object collection is a collection that directly contains model instances,
2485
+ each containing at least one serialized attribute.
2486
+
2487
+ .Simple root collection in YAML with each instance being a models with an attribute `name`
2488
+ [example]
2489
+ ====
2490
+ [source,yaml]
2491
+ ----
2492
+ ---
2493
+ - name: Item One
2494
+ - name: Item Two
2495
+ - name: Item Three
2496
+ ----
2497
+
2498
+ [source,json]
2499
+ ----
2500
+ [
2501
+ {"name": "Item One"},
2502
+ {"name": "Item Two"},
2503
+ {"name": "Item Three"}
2504
+ ]
2505
+ ----
2506
+ ====
2507
+
2508
+
2509
+ .Handling a root collection where each instance is defined by a model with attributes
2510
+ [example]
2511
+ ====
2512
+ Code:
2513
+
2514
+ [source,ruby]
2515
+ ----
2516
+ class Title < Lutaml::Model::Serializable
2517
+ attribute :content, :string
2518
+ end
2519
+
2520
+ class TitleCollection < Lutaml::Model::Collection
2521
+ instances :titles, Title
2522
+
2523
+ key_value do
2524
+ no_root # default
2525
+ map_instances to: :titles
2526
+ end
2527
+ end
2528
+ ----
2529
+
2530
+ Data:
2531
+
2532
+ [source,yaml]
2533
+ ----
2534
+ ---
2535
+ - content: Title One
2536
+ - content: Title Two
2537
+ - content: Title Three
2538
+ ----
2539
+
2540
+ [source,json]
2541
+ ----
2542
+ [
2543
+ {"content": "Title One"},
2544
+ {"content": "Title Two"},
2545
+ {"content": "Title Three"}
2546
+ ]
2547
+ ----
2548
+
2549
+ Usage:
2550
+
2551
+ [source,ruby]
2552
+ ----
2553
+ titles = TitleCollection.from_yaml(yaml_data)
2554
+ titles.count
2555
+ # => 3
2556
+ titles.first.content
2557
+ # => "Title One"
2558
+ ----
2559
+ ====
2560
+
2561
+
2562
+ === Named collections
2563
+
2564
+ ==== General
2565
+
2566
+ Named collections are collections wrapped inside a name or a key. The "name" of
2567
+ the collection serves as the container root of its contained model instances.
2568
+
2569
+ The named collection setup applies to XML and key-value serialization formats.
2570
+
2571
+ In a named collection setup, the collection is defined as a
2572
+ Lutaml::Model::Collection class, and each instance is defined as a
2573
+ Lutaml::Model::Serializable class.
2574
+
2575
+ There are two kinds of named collections depending on the type of the instance
2576
+ value:
2577
+
2578
+ "Named value collection":: the value is a "primitive type"
2579
+
2580
+ "Named object collection":: the value is a "model instance"
2581
+
2582
+ Regardless of the name of root collection, the instance in a collection is
2583
+ always a LutaML model instance.
2584
+
2585
+
2586
+ ==== Named value collections
2587
+
2588
+ A named value collection is a collection that contains values of a
2589
+ primitive type.
2590
+
2591
+ .Named value collection in XML with models each containing an element with content
2592
+ [source,xml]
2593
+ ----
2594
+ <names>
2595
+ <name>Item One</name>
2596
+ <name>Item Two</name>
2597
+ <name>Item Three</name>
2598
+ </names>
2599
+ ----
2600
+
2601
+ .Named value collection in YAML with models each containing a value
2602
+ [source,yaml]
2603
+ ----
2604
+ ---
2605
+ names:
2606
+ - Item One
2607
+ - Item Two
2608
+ - Item Three
2609
+ ----
2610
+
2611
+ Syntax:
2612
+
2613
+ [source,ruby]
2614
+ ----
2615
+ class MyCollection < Lutaml::Model::Collection
2616
+ instances :items, ModelType
2617
+
2618
+ xml do
2619
+ root "name-of-xml-container-element"
2620
+ end
2621
+
2622
+ key_value do
2623
+ root "name-of-key-value-container-element"
2624
+ end
2625
+ end
2626
+
2627
+ class ModelType < Lutaml::Model::Serializable
2628
+ attribute :name, :string
2629
+ end
2630
+ ----
2631
+
2632
+ A named collection can alternatively be implemented as a non-collection model
2633
+ ("Model class with an attribute") that contains the collection of instances. In
2634
+ this case, the attribute will be an Array object, which does not contain
2635
+ additional attributes and methods.
2636
+
2637
+ .Handling a named collection with instance elements directly containing values
2638
+ [example]
2639
+ ====
2640
+ [source,ruby]
2641
+ ----
2642
+ class Title < Lutaml::Model::Serializable
2643
+ attribute :title, :string
2644
+
2645
+ xml do
2646
+ root "title"
2647
+ map_content to: :title
2648
+ end
2649
+ end
2650
+
2651
+ class DirectTitleCollection < Lutaml::Model::Collection
2652
+ instances :items, Title
2653
+
2654
+ xml do
2655
+ root "titles"
2656
+ map_element "title", to: :items
2657
+ end
2658
+
2659
+ key_value do
2660
+ map_instances to: :items
2661
+ end
2662
+ end
2663
+ ----
2664
+
2665
+ [source,xml]
2666
+ ----
2667
+ <titles>
2668
+ <title>Title One</title>
2669
+ <title>Title Two</title>
2670
+ <title>Title Three</title>
2671
+ </titles>
2672
+ ----
2673
+
2674
+ [source,yaml]
2675
+ ----
2676
+ ---
2677
+ titles:
2678
+ - Title One
2679
+ - Title Two
2680
+ - Title Three
2681
+ ----
2682
+
2683
+ [source,json]
2684
+ ----
2685
+ {
2686
+ "titles": [
2687
+ "Title One",
2688
+ "Title Two",
2689
+ "Title Three"
2690
+ ]
2691
+ }
2692
+ ----
2693
+
2694
+ [source,ruby]
2695
+ ----
2696
+ titles = DirectTitleCollection.from_yaml(yaml_data)
2697
+ titles.count
2698
+ # => 3
2699
+ titles.first.title
2700
+ # => "Title One"
2701
+ titles.last.title
2702
+ # => "Title Three"
2703
+ ----
2704
+ ====
2705
+
2706
+
2707
+ ==== Named object collections
2708
+
2709
+ A named object collection is a collection that contains model instances,
2710
+ each containing at least one serialized attribute.
2711
+
2712
+ NOTE: A named object collection can alternatively be implemented as a
2713
+ non-collection model ("Model class with an attribute") that contains the
2714
+ collection of instances. In this case, the attribute will be an Array object,
2715
+ which does not contain additional attributes and methods.
2716
+
2717
+
2718
+ .Named object collection in XML with instances each containing an element with a model attribute
2719
+ [source,xml]
2720
+ ----
2721
+ <names>
2722
+ <name><content>Item One</content></name>
2723
+ <name><content>Item Two</content></name>
2724
+ <name><content>Item Three</content></name>
2725
+ </names>
2726
+ ----
2727
+
2728
+ .Named object collection in YAML with instances each containing a model attribute
2729
+ [source,yaml]
2730
+ ----
2731
+ ---
2732
+ names:
2733
+ - name: Item One
2734
+ - name: Item Two
2735
+ - name: Item Three
2736
+ ----
2737
+
2738
+
2739
+ .Named object collection with each instance containing at least one model attribute
2740
+ [example]
2741
+ ====
2742
+ Data:
2743
+
2744
+ [source,xml]
2745
+ ----
2746
+ <titles>
2747
+ <title><content>Title One</content></title>
2748
+ <title><content>Title Two</content></title>
2749
+ <title><content>Title Three</content></title>
2750
+ </titles>
2751
+ ----
2752
+
2753
+ [source,yaml]
2754
+ ----
2755
+ ---
2756
+ titles:
2757
+ - title: Title One
2758
+ - title: Title Two
2759
+ - title: Title Three
2760
+ ----
2761
+
2762
+ [source,json]
2763
+ ----
2764
+ {
2765
+ "titles": [
2766
+ {"title": "Title One"},
2767
+ {"title": "Title Two"},
2768
+ {"title": "Title Three"}
2769
+ ]
2770
+ }
2771
+ ----
2772
+
2773
+ Code:
2774
+
2775
+ [source,ruby]
2776
+ ----
2777
+ class Title < Lutaml::Model::Serializable
2778
+ attribute :title, :string
2779
+
2780
+ xml do
2781
+ root "title"
2782
+ map_element "content", to: :title
2783
+ end
2784
+
2785
+ key_value do
2786
+ map "title", to: :title
2787
+ end
2788
+ end
2789
+
2790
+ class TitleCollection < Lutaml::Model::Collection
2791
+ instances :items, Title
2792
+
2793
+ xml do
2794
+ root "titles"
2795
+ map_element 'title', to: :items
2796
+ end
2797
+
2798
+ key_value do
2799
+ root "titles"
2800
+ map_instances to: :items
2801
+ end
2802
+ end
2803
+ ----
2804
+
2805
+ Usage:
2806
+
2807
+ [source,ruby]
2808
+ ----
2809
+ titles = TitleCollection.from_yaml(yaml_data)
2810
+ titles.count
2811
+ # => 3
2812
+ titles.first.title
2813
+ # => "Title One"
2814
+ titles.last.title
2815
+ # => "Title Three"
2816
+ ----
2817
+ ====
2818
+
2819
+
2820
+ === Attribute collection class
2821
+
2822
+ A model attribute that is a collection can be contained within a custom
2823
+ collection class.
2824
+
2825
+ A custom collection class can be defined to provide custom behavior for the
2826
+ collection inside a non-collection model, with attributes using
2827
+ `collection: true`.
2828
+
2829
+ Syntax:
2830
+
2831
+ [source,ruby]
2832
+ ----
2833
+ class MyModel < Lutaml::Model::Serializable
2834
+ attribute {model-attribute}, ModelType, collection: MyCollection
2835
+ end
2836
+
2837
+ class MyCollection < Lutaml::Model::Collection
2838
+ instances {instance-name}, ModelType
2839
+
2840
+ # Custom behavior for the collection
2841
+ def custom_method
2842
+ # Custom logic here
2843
+ end
2844
+ end
2845
+ ----
2846
+
2847
+ .Using a custom collection class for custom collection behavior
2848
+ [example]
2849
+ ====
2850
+ Data:
2851
+
2852
+ [source,xml]
2853
+ ----
2854
+ <titles>
2855
+ <title>Title One</title>
2856
+ <title>Title Two</title>
2857
+ <title>Title Three</title>
2858
+ </titles>
2859
+ ----
2860
+
2861
+ [source,yaml]
2862
+ ----
2863
+ titles:
2864
+ - title: Title One
2865
+ - title: Title Two
2866
+ - title: Title Three
2867
+ ----
2868
+
2869
+
2870
+ [source,ruby]
2871
+ ----
2872
+ class StringParts < Lutaml::Model::Collection
2873
+ instances :parts, :string
2874
+
2875
+ def to_s
2876
+ parts.join(' -- ')
2877
+ end
2878
+ end
2879
+
2880
+ class BibliographicItem < Lutaml::Model::Serializable
2881
+ attribute :title_parts, :string, collection: StringParts
2882
+
2883
+ xml do
2884
+ root "titles"
2885
+ map_element "title", to: :title_parts
2886
+ end
2887
+
2888
+ key_value do
2889
+ root "titles"
2890
+ map_instances to: :title_parts
2891
+ end
2892
+
2893
+ def render_title
2894
+ title_parts.to_s
2895
+ end
2896
+ end
2897
+ ----
2898
+
2899
+ [source,ruby]
2900
+ ----
2901
+ > bib_item = BibliographicItem.from_xml(xml_data)
2902
+ > bib_item.title_parts
2903
+ > # StringParts:0x0000000104ac7240 @parts=["Title One", "Title Two", "Title Three"]
2904
+ > bib_item.render_title
2905
+ > # "Title One -- Title Two -- Title Three"
2906
+ ----
2907
+ ====
2908
+
2909
+
2910
+
2911
+ === Nested collections
2912
+
2913
+ TODO: This case needs to be fixed.
2914
+
2915
+ Collections can be nested within other models and define their own serialization
2916
+ rules.
2917
+
2918
+ Nested collections can be defined in the same way as root collections, but
2919
+ they are defined within the context of a parent model.
2920
+
2921
+ [example]
2922
+ ====
2923
+ Data:
2924
+
2925
+ [source,xml]
2926
+ ----
2927
+ <titles>
2928
+ <title-group>
2929
+ <artifact>
2930
+ <content>Title One</content>
2931
+ </artifact>
2932
+ <artifact>
2933
+ <content>Title Two</content>
2934
+ </artifact>
2935
+ <artifact>
2936
+ <content>Title Three</content>
2937
+ </artifact>
2938
+ </title-group>
2939
+ </titles>
2940
+ ----
2941
+
2942
+ [source,yaml]
2943
+ ----
2944
+ ---
2945
+ titles:
2946
+ title-group:
2947
+ - artifact:
2948
+ content: Title One
2949
+ - artifact:
2950
+ content: Title Two
2951
+ - artifact:
2952
+ content: Title Three
2953
+ ----
2954
+
2955
+ [source,ruby]
2956
+ ----
2957
+ class Title < Lutaml::Model::Serializable
2958
+ attribute :content, :string
2959
+ end
2960
+
2961
+ class TitleCollection < Lutaml::Model::Collection
2962
+ instances :items, Title
2963
+
2964
+ xml do
2965
+ root "title-group"
2966
+ map_element "artifact", to: :items
2967
+ end
2968
+ end
2969
+
2970
+ class BibItem < Lutaml::Model::Serializable
2971
+ attribute :titles, TitleCollection
2972
+
2973
+ xml do
2974
+ root "bibitem"
2975
+ # This overrides the collection's root "title-group"
2976
+ map_element "titles", to: :titles
2977
+ end
2978
+ end
2979
+ ----
2980
+ ====
2981
+
2982
+
2983
+ === Keyed collections (key-value serialization formats only)
2984
+
2985
+ ==== General
2986
+
2987
+ In key-value serialization formats, a key can be used to uniquely identify each
2988
+ instance. This usage allows for enforcing uniqueness in the collection.
2989
+
2990
+ A collection that contains keyed objects as its instances is commonly called a
2991
+ "keyed collection". A keyed object in a serialization format is an object
2992
+ identified with a unique key.
2993
+
2994
+ NOTE: The concept of keyed collections does not typically apply to XML
2995
+ collections.
2996
+
2997
+ There are two kinds of keyed collections depending on the type of the keyed
2998
+ value:
2999
+
3000
+ "keyed value collection":: the value is a "primitive type"
3001
+
3002
+ "keyed object collection":: the value is a "model instance"
3003
+
3004
+ Regardless of the type of keyed collections, the instance in a collection is
3005
+ always a LutaML model instance.
3006
+
3007
+
3008
+ ==== `map_key` method
3009
+
3010
+ The `map_key` method specifies that the unique key is to be moved into an
3011
+ attribute belonging to the instance model.
3012
+
3013
+ Syntax:
3014
+
3015
+ [source,ruby]
3016
+ ----
3017
+ key_value do
3018
+ map_key to_instance: {instance-attribute-name}
3019
+ end
3020
+ ----
3021
+
3022
+ Where,
3023
+
3024
+ `to_instance`:: Refers to the attribute name in the instance that contains the key.
3025
+ `{key_attribute}`:: The attribute name in the instance that contains the key.
3026
+
3027
+
3028
+ ==== `map_value` method
3029
+
3030
+ The `map_value` method specifies that the value (the object referenced by the
3031
+ unique key) is to be moved into an attribute belonging to the instance model.
3032
+
3033
+ Syntax:
3034
+
3035
+ [source,ruby]
3036
+ ----
3037
+ key_value do
3038
+ # basic pattern
3039
+ map_value {operation}: [*argument]
3040
+
3041
+ # Mapping the value object to a full instance through `to_instance`
3042
+ map_value to_instance: {instance-attribute-name}
3043
+
3044
+ # Mapping the value object to an attribute as_instance
3045
+ map_value as_attribute: {instance-attribute-name}
3046
+ end
3047
+ ----
3048
+
3049
+ ==== Keyed value collections
3050
+
3051
+ A keyed value collection is a collection where the keyed item in the serialization
3052
+ format is a primitive type (e.g. string, integer, etc.).
3053
+
3054
+ The instance item inside the collection is a model instance that contains both
3055
+ the serialized key and serialized value both as attributes inside the model.
3056
+
3057
+ All three `map_key`, `map_value`, and `map_instances` methods need to be used to
3058
+ define how instances are mapped in a keyed value collection.
3059
+
3060
+ .Creating a keyed value collection
3061
+ [example]
3062
+ ====
3063
+ [source,ruby]
3064
+ ----
3065
+ class AuthorAvailability < Lutaml::Model::Serializable
3066
+ attribute :id, :string
3067
+ attribute :available, :boolean
3068
+ end
3069
+
3070
+ class AuthorCollection < Lutaml::Model::Collection
3071
+ instances :authors, AuthorAvailability
3072
+
3073
+ key_value do
3074
+ map_key to_instance: :id # This refers to 'authors[].id'
3075
+ map_value as_attribute: :available # This refers to 'authors[].available'
3076
+ map_instances to: :authors
3077
+ end
3078
+ end
3079
+ ----
3080
+
3081
+ [source,yaml]
3082
+ ----
3083
+ ---
3084
+ author_01: true
3085
+ author_02: false
3086
+ author_03: true
3087
+ ----
3088
+
3089
+ [source,ruby]
3090
+ ----
3091
+ authors = AuthorCollection.from_yaml(yaml_data)
3092
+ authors.first.id
3093
+ # => "author_01"
3094
+ authors.first.available
3095
+ # => true
3096
+ ----
3097
+ ====
3098
+
3099
+
3100
+ ==== Keyed object collections
3101
+
3102
+ A keyed object collection is a collection where the keyed item in the
3103
+ serialization format contains multiple attributes.
3104
+
3105
+ The instance item inside the collection is a model instance that contains the
3106
+ serialized key as one attribute, and the serialized value attributes are all
3107
+ attributes inside the model.
3108
+
3109
+ Both the `map_key` and `map_instances` are used to define how instances are
3110
+ mapped in a keyed object collection.
3111
+
3112
+ .Creating a keyed object collection
3113
+ [example]
3114
+ ====
3115
+ [source,ruby]
3116
+ ----
3117
+ class Author < Lutaml::Model::Serializable
3118
+ attribute :id, :string
3119
+ attribute :name, :string
3120
+ end
3121
+
3122
+ class AuthorCollection < Lutaml::Model::Collection
3123
+ instances :authors, Author
3124
+
3125
+ key_value do
3126
+ map_key to_instance: :id # This refers to 'authors[].id'
3127
+ map_instances to: :authors
3128
+ end
3129
+ end
3130
+ ----
3131
+
3132
+ [source,yaml]
3133
+ ----
3134
+ ---
3135
+ author_01:
3136
+ name: Author One
3137
+ author_02:
3138
+ name: Author Two
3139
+ author_03:
3140
+ name: Author Three
3141
+ ----
3142
+
3143
+ [source,ruby]
3144
+ ----
3145
+ authors = AuthorCollection.from_yaml(yaml_data)
3146
+ authors.first.id
3147
+ # => "author_01"
3148
+ authors.first.name
3149
+ # => "Author One"
3150
+ ----
3151
+ ====
3152
+
3153
+
3154
+ ==== Nested keyed object collection
3155
+
3156
+ A nested keyed object collection is a keyed collection that contain other keyed
3157
+ collections. This case is simply a more complex arrangement of the principles
3158
+ applied to keyed object collections.
3159
+
3160
+ This pattern can extend to multiple levels of nesting, where each level contains
3161
+ a keyed object collection that can have its own key and value mappings.
3162
+
3163
+ Depends on whether a custom collection class is needed, the following
3164
+ mechanisms are available:
3165
+
3166
+ * When using a Lutaml::Model::Serializable class for a keyed collection,
3167
+ use the `child_mappings` option to map attributes.
3168
+
3169
+ * When using a Lutaml::Model::Collection class for a keyed collection,
3170
+ there are two options:
3171
+
3172
+ * use the `map_key`, `map_value`, and `map_instances` methods to map attributes;
3173
+ or
3174
+
3175
+ * use the `root_mappings` option to map attributes.
3176
+
3177
+
3178
+ .Nested 2-layer keyed object collection
3179
+ [example]
3180
+ ====
3181
+ This example provides a two-layer nested structure where:
3182
+
3183
+ * The first layer keys pieces by type (`bowls`, `vases`).
3184
+ * The second layer keys glazes by finish name within each piece type.
3185
+ * Each glaze finish contains detailed attributes like temperature.
3186
+
3187
+ [source,ruby]
3188
+ ----
3189
+ # Third layer represents glaze finishes.
3190
+ class GlazeFinish < Lutaml::Model::Serializable
3191
+ attribute :name, :string
3192
+ attribute :temperature, :integer
3193
+
3194
+ key_value do
3195
+ map "name", to: :name
3196
+ map "temperature", to: :temperature
3197
+ end
3198
+ end
3199
+
3200
+ # Second layer represents ceramic pieces each with multiple finishes.
3201
+ class CeramicPiece < Lutaml::Model::Serializable
3202
+ attribute :piece_type, :string
3203
+ attribute :glazes, GlazeFinish, collection: true
3204
+
3205
+ key_value do
3206
+ map "piece_type", to: :piece_type
3207
+ map "glazes", to: :glazes, child_mappings: {
3208
+ name: :key,
3209
+ temperature: :temperature
3210
+ }
3211
+ end
3212
+ end
3213
+
3214
+ # Uppermost layer represents the collection of ceramic pieces.
3215
+ class StudioInventory < Lutaml::Model::Collection
3216
+ instances :pieces, CeramicPiece
3217
+
3218
+ key_value do
3219
+ map to: :pieces, root_mappings: {
3220
+ piece_type: :key,
3221
+ glazes: :value,
3222
+ }
3223
+ end
3224
+ end
3225
+ ----
3226
+
3227
+ [source,yaml]
3228
+ ----
3229
+ ---
3230
+ bowls:
3231
+ matte_finish:
3232
+ name: Earth Matte
3233
+ temperature: 1240
3234
+ glossy_finish:
3235
+ name: Ocean Blue
3236
+ temperature: 1260
3237
+ crackle_finish:
3238
+ name: Antique Crackle
3239
+ temperature: 1220
3240
+ vases:
3241
+ metallic_finish:
3242
+ name: Bronze Metallic
3243
+ temperature: 1280
3244
+ crystalline_finish:
3245
+ name: Ice Crystal
3246
+ temperature: 1300
3247
+ ----
3248
+
3249
+ [source,ruby]
3250
+ ----
3251
+ inventory = StudioInventory.from_yaml(yaml_data)
3252
+
3253
+ # Access nested data through the hierarchy
3254
+ puts inventory.pieces.bowls.matte_finish.name
3255
+ # => "Earth Matte"
3256
+
3257
+ puts inventory.pieces.bowls.matte_finish.temperature
3258
+ # => 1240
3259
+
3260
+ # Iterate through all pieces and their glazes
3261
+ inventory.pieces.each do |piece_type, piece|
3262
+ puts "#{piece_type.capitalize}:"
3263
+ piece.glazes.each do |glaze_name, glaze|
3264
+ puts " #{glaze_name}: #{glaze.name} (#{glaze.temperature}°C)"
3265
+ end
3266
+ end
3267
+ ----
3268
+ ====
3269
+
3270
+
3271
+
3272
+
3273
+ === Behavior
3274
+
3275
+ ==== Enumerable interface
3276
+
3277
+ Collections implement the Ruby `Enumerable` interface, providing standard
3278
+ collection operations.
3279
+
3280
+ Collections allow the following sample `Enumerable` methods:
3281
+
3282
+ * `each` - Iterate over collection items
3283
+ * `map` - Transform collection items
3284
+ * `select` - Filter collection items
3285
+ * `find` - Find items matching criteria
3286
+ * `reduce` - Aggregate collection items
3287
+
3288
+ .Usage of the collection Enumerable interface
3289
+ [example]
3290
+ ====
3291
+ [source,ruby]
3292
+ ----
3293
+ # Filter items
3294
+ filtered = collection.filter { |item| item.id == "1" }
3295
+
3296
+ # Reject items
3297
+ rejected = collection.reject { |item| item.id == "1" }
3298
+
3299
+ # Select items
3300
+ selected = collection.select { |item| item.id == "1" }
3301
+
3302
+ # Map items
3303
+ mapped = collection.map { |item| item.name }
3304
+
3305
+ # Count items
3306
+ count = collection.count
3307
+ ----
3308
+ ====
3309
+
3310
+
3311
+ // ==== Collection validation
3312
+
3313
+ // Collections can define validation rules for their elements.
3314
+
3315
+ // [example]
3316
+ // ====
3317
+ // [source,ruby]
3318
+ // ----
3319
+ // class PublicationCollection < Lutaml::Model::Collection
3320
+ // instances(:publications, Publication) do
3321
+ // validates :year, numericality: { greater_than: 1900 }
3322
+
3323
+ // validate :must_have_author
3324
+
3325
+ // def must_have_author(publications)
3326
+ // publications.each do |publication|
3327
+ // next unless publication.author.nil?
3328
+ // errors.add(:author, "`#{publication.title}` must have an author")
3329
+ // end
3330
+ // end
3331
+ // end
3332
+ // end
3333
+ // ----
3334
+ // ====
3335
+
3336
+ ==== Initialization
3337
+
3338
+ Collections can be initialized with an array of items or through individual item
3339
+ addition.
3340
+
3341
+ [example]
3342
+ ====
3343
+ [source,ruby]
3344
+ ----
3345
+ # Empty collection
3346
+ collection = ItemCollection.new
3347
+
3348
+ # From an array of items
3349
+ collection = ItemCollection.new([item1, item2, item3])
3350
+
3351
+ # From an array of hashes
3352
+ collection = ItemCollection.new([
3353
+ { id: "1", name: "Item 1" },
3354
+ { id: "2", name: "Item 2" }
3355
+ ])
3356
+
3357
+ # Adding items later
3358
+ collection << Item.new(id: "3", name: "Item 3")
3359
+ ----
3360
+ ====
3361
+
3362
+ ==== Ordering
3363
+
3364
+ TODO: This case needs to be fixed.
3365
+
3366
+ Collections that maintain a specific ordering of elements.
3367
+
3368
+ Syntax:
3369
+
3370
+ [source,ruby]
3371
+ ----
3372
+ class MyCollection < Lutaml::Model::Collection
3373
+ instances {instances-name}, ModelType
3374
+ ordered by: {attribute-of-instance}, order: {:asc | :desc}
3375
+ end
3376
+ ----
3377
+
3378
+ Where,
3379
+
3380
+ `{instances-name}`:: name of the instances accessor within the collection
3381
+ `ModelType`:: The model type of the collection elements.
3382
+
3383
+ `{attribute-of-instance-or-proc}`:: How model instances are to be ordered by. Values supported are:
3384
+ `{attribute-of-instance}`::: Attribute name of an instance to be ordered by.
3385
+ `{proc}`::: Proc that returns a value to order by (same as `sort_by`), given the instance as input.
3386
+
3387
+ `order`::: Order direction of the value:
3388
+ `:asc`:::: Ascending order (default).
3389
+ `:desc`:::: Descending order.
3390
+
3391
+ .Ordered collection applied to a root collection
3392
+ [example]
3393
+ ====
3394
+ Data:
3395
+
3396
+ [source,xml]
3397
+ ----
3398
+ <items>
3399
+ <item id="3" name="Item Three"/>
3400
+ <item id="1" name="Item One"/>
3401
+ <item id="2" name="Item Two"/>
3402
+ </items>
3403
+ ----
3404
+
3405
+ [source,yaml]
3406
+ ----
3407
+ ---
3408
+ - id: 3
3409
+ name: Item Three
3410
+ - id: 1
3411
+ name: Item One
3412
+ - id: 2
3413
+ name: Item Two
3414
+ ----
3415
+
3416
+ [source,ruby]
3417
+ ----
3418
+ class Item < Lutaml::Model::Serializable
3419
+ attribute :id, :string
3420
+ attribute :name, :string
3421
+
3422
+ xml do
3423
+ map_attribute "id", to: :id
3424
+ map_attribute "name", to: :name
3425
+ end
3426
+ end
3427
+
3428
+ class OrderedItemCollection < Lutaml::Model::Collection
3429
+ instances :items, Item
3430
+ ordered by: :id, order: :desc
3431
+
3432
+ xml do
3433
+ root "items"
3434
+ map_element "item", to: :items
3435
+ end
3436
+
3437
+ key_value do
3438
+ no_root
3439
+ map_instances to: :items
3440
+ end
3441
+ end
3442
+ ----
3443
+
3444
+ [source,ruby]
3445
+ ----
3446
+ > collection = OrderedItemCollection.from_xml(xml_data)
3447
+ > collection.map(&:id)
3448
+ > # ["3", "2", "1"]
3449
+
3450
+ > collection = OrderedItemCollection.from_yaml(yaml_data)
3451
+ > collection.map(&:id)
3452
+ > # ["3", "2", "1"]
3453
+ ----
3454
+ ====
3455
+
3456
+
3457
+
3458
+ // ==== Polymorphic collections
3459
+
3460
+ // Collections can contain instances of different model classes that share a common
3461
+ // base class.
3462
+
3463
+ // The polymorphic options for attributes are also applied here.
3464
+
3465
+ // [example]
3466
+ // ====
3467
+ // [source,ruby]
3468
+ // ----
3469
+ // class PolymorphicItemCollection < Lutaml::Model::Collection
3470
+ // instances :items, Item, polymorphic: true
3471
+
3472
+ // xml do
3473
+ // root "items"
3474
+ // map_element "item", to: :items
3475
+ // end
3476
+
3477
+ // key_value do
3478
+ // root "items"
3479
+ // map_instances to: :items
3480
+ // end
3481
+ // end
3482
+ // ----
3483
+ // ====
3484
+
3485
+
3486
+ == Serialization model mappings
3487
+
3488
+ === General
3489
+
3490
+ Lutaml::Model allows you to translate a data model into serialization models of
3491
+ various serialization formats.
3492
+
3493
+ Depending on the serialization format, different methods are supported for
3494
+ defining serialization and deserialization mappings.
3495
+
3496
+ A serialization model mapping is defined using a format-specific DSL block
3497
+ in this syntax:
3498
+
3499
+ [source,ruby]
3500
+ ----
3501
+ class Example < Lutaml::Model::Serializable
3502
+ {format-short-name} do <1>
3503
+ # ...
3504
+ end
3505
+ end
3506
+ ----
3507
+ <1> `{format-short-name}` is the serialization format short name.
3508
+
3509
+ There are two kinds of serialization models:
3510
+
3511
+ * Represents a singular model (maps to a Lutaml::Model::Serializable)
3512
+ * Represents a group/collection of models (maps to Lutaml::Model::Collection)
3513
+
3514
+ A collection contains instances of singular models, and therefore is always
3515
+ inextricably linked to an underlying serialization format for singular models.
3516
+ For instance, JSONL represents a collection (itself being invalid JSON) that
3517
+ uses JSON for singular models.
3518
+
3519
+ The supported serialization formats and their short names are defined as follows:
3520
+
3521
+ Model serialization formats::
3522
+
3523
+ `xml`::: XML
3524
+ `hsh`::: Hash
3525
+ +
3526
+ NOTE: Yes a 3-letter abbreviation for Hash!
3527
+
3528
+ `json`::: JSON
3529
+ `yaml`::: YAML
3530
+ `toml`::: TOML
3531
+ `key_value`::: Key-value format, a shorthand for all key-value formats (including
3532
+ JSON, YAML and TOML).
3533
+
3534
+ Collection serialization formats::
3535
+
3536
+ `jsonl`::: JSONL (JSON Lines)
3537
+ `yamls`::: YAML Stream (multi-document format)
3538
+
3539
+
3540
+ .Using the `xml`, `hsh`, `json`, `yaml`, `toml` and `key_value` blocks to define serialization mappings
3541
+ [example]
3542
+ ====
3543
+ [source,ruby]
3544
+ ----
3545
+ class Example < Lutaml::Model::Serializable
3546
+ xml do
3547
+ # ...
3548
+ end
3549
+
3550
+ hsh do
3551
+ # ...
3552
+ end
3553
+
3554
+ json do
3555
+ # ...
3556
+ end
3557
+
3558
+ yaml do
3559
+ # ...
3560
+ end
3561
+
3562
+ toml do
3563
+ # ...
3564
+ end
3565
+
3566
+ key_value do
3567
+ # ...
3568
+ end
3569
+ end
3570
+ ----
3571
+ ====
3572
+
3573
+ .Using the `jsonl` block to define serialization mappings to a collection
3574
+ [example]
3575
+ ====
3576
+ [source,ruby]
3577
+ ----
3578
+ class Example < Lutaml::Model::Collection
3579
+ jsonl do
3580
+ # ...
3581
+ end
3582
+ end
3583
+ ----
3584
+ ====
3585
+
3586
+
3587
+ === XML
3588
+
3589
+ ==== Setting root element name
3590
+
3591
+ The `root` method sets the root element tag name of the XML document.
3592
+
3593
+ If `root` is not given, then the snake-cased class name will be used as the
3594
+ root.
3595
+
3596
+ [example]
3597
+ Sets the tag name for `<example>` in XML `<example>...</example>`.
3598
+
3599
+ Syntax:
3600
+
3601
+ [source,ruby]
3602
+ ----
3603
+ xml do
3604
+ root 'xml_element_name'
3605
+ end
3606
+ ----
3607
+
3608
+ .Setting the root element name to `example`
3609
+ [example]
3610
+ ====
3611
+ [source,ruby]
3612
+ ----
3613
+ class Example < Lutaml::Model::Serializable
3614
+ xml do
3615
+ root 'example'
3616
+ end
3617
+ end
3618
+ ----
3619
+
3620
+ [source,ruby]
3621
+ ----
3622
+ > Example.new.to_xml
3623
+ > #<example></example>
3624
+ ----
3625
+ ====
3626
+
3627
+
3628
+ ==== Ommiting root element
3629
+
3630
+ The root element can be omitted by using the `no_root` method.
3631
+
3632
+ When `no_root` is used, only `map_element` can be used because without a root
3633
+ element there cannot be attributes.
3634
+
3635
+ Syntax:
3636
+
3637
+ [source,ruby]
3638
+ ----
3639
+ xml do
3640
+ no_root
3641
+ end
3642
+ ----
3643
+
3644
+ [example]
3645
+ ====
3646
+ [source,ruby]
3647
+ ----
3648
+ class NameAndCode < Lutaml::Model::Serializable
3649
+ attribute :name, :string
3650
+ attribute :code, :string
3651
+
3652
+ xml do
3653
+ no_root
3654
+ map_element "code", to: :code
3655
+ map_element "name", to: :name
3656
+ end
3657
+ end
3658
+ ----
3659
+
3660
+ [source,xml]
3661
+ ----
3662
+ <name>Name</name>
3663
+ <code>ID-001</code>
3664
+ ----
3665
+
3666
+ [source,ruby]
3667
+ ----
3668
+ > parsed = NameAndCode.from_xml(xml)
3669
+ > # <NameAndCode:0x0000000107a3ca70 @code="ID-001", @name="Name">
3670
+ > parsed.to_xml
3671
+ > # <code>ID-001</code><name>Name</name>
3672
+ ----
3673
+ ====
3674
+
3675
+
3676
+
3677
+ [[xml-map-all]]
3678
+ ==== Mapping all XML content
3679
+
3680
+ The `map_all` tag in XML mapping captures and maps all content within an XML
3681
+ element into a single attribute in the target Ruby object.
3682
+
3683
+ The use case for `map_all` is to tell Lutaml::Model to not parse the content of
3684
+ the XML element at all, and instead handle it as an XML string.
3685
+
3686
+ NOTE: The corresponding method for key-value formats is at <<key-value-map-all>>.
3687
+
3688
+ WARNING: Notice that usage of mapping all will lead to incompatibility between
3689
+ serialization formats, i.e. the raw string content will not be portable as
3690
+ objects are across different formats.
3691
+
3692
+ This is useful in the case where the content of an XML element is not to be
3693
+ handled by a Lutaml::Model::Serializable object.
3694
+
3695
+ This feature is commonly used with custom methods or a custom model object to
3696
+ handle the content.
2127
3697
 
2128
3698
  This includes:
2129
3699
 
@@ -2983,6 +4553,7 @@ HERE
2983
4553
  ----
2984
4554
  ====
2985
4555
 
4556
+ NOTE: For importing model mappings inside a `sequence` block, refer to <<import-model-mappings-inside-sequence, Importing model mappings inside a `sequence`>>.
2986
4557
 
2987
4558
  [[xml-schema-location]]
2988
4559
  ==== Automatic support of `xsi:schemaLocation`
@@ -3448,11 +5019,20 @@ This XML snippet is in UTF-8.
3448
5019
 
3449
5020
  ==== General
3450
5021
 
3451
- Key-value data models like JSON, YAML, and TOML all share a similar structure
3452
- where data is stored as key-value pairs.
5022
+ Key-value data models share a similar structure where data is stored as
5023
+ key-value pairs.
3453
5024
 
3454
5025
  `Lutaml::Model` works with these formats in a similar way.
3455
5026
 
5027
+ Key-value data models supported are identified by their short name:
5028
+
5029
+ `hsh`:: Hash (Ruby `Hash` class)
5030
+ `json`:: JSON
5031
+ `yaml`:: YAML
5032
+ `toml`:: TOML
5033
+ `key_value`:: A way to configure key-value mappings for all supported key-value data models.
5034
+
5035
+
3456
5036
  ==== Mapping
3457
5037
 
3458
5038
  The `map` method is used to define key-value mappings.
@@ -3461,16 +5041,40 @@ Syntax:
3461
5041
 
3462
5042
  [source,ruby]
3463
5043
  ----
3464
- json | yaml | toml | key_value do
5044
+ {key_value_type_short} do <1>
3465
5045
  map 'key_value_model_attribute_name', to: :name_of_attribute
3466
5046
  end
3467
5047
  ----
5048
+ <1> `key_value_type_short` is the key-value data model's short name.
5049
+
5050
+ .Creating a key-value data model mapping for only the JSON format
5051
+ [example]
5052
+ ====
5053
+ [source,ruby]
5054
+ ----
5055
+ json do
5056
+ map :color, to: :color
5057
+ map :desc, to: :description
5058
+ end
5059
+ ----
5060
+ ====
3468
5061
 
5062
+ .Creating a key-value data model mapping for all key-value formats
5063
+ [example]
5064
+ ====
5065
+ [source,ruby]
5066
+ ----
5067
+ key_value do
5068
+ map :color, to: :color
5069
+ map :desc, to: :description
5070
+ end
5071
+ ----
5072
+ ====
3469
5073
 
3470
5074
  ==== Unified mapping
3471
5075
 
3472
5076
  The `key_value` method is a streamlined way to map all attributes for
3473
- serialization into key-value formats including JSON, YAML, and TOML.
5077
+ serialization into key-value formats including Hash, JSON, YAML, and TOML.
3474
5078
 
3475
5079
  If there is no definite differentiation between the key value formats, the
3476
5080
  `key_value` method simplifies defining mappings and improves code readability.
@@ -3489,19 +5093,11 @@ class CeramicModel < Lutaml::Model::Serializable
3489
5093
  attribute :glaze, :string
3490
5094
  attribute :description, :string
3491
5095
 
3492
- key_value do
3493
- map :color, to: color
5096
+ key_value do # or any other key-value data model
5097
+ map :color, to: :color
3494
5098
  map :glz, to: :glaze
3495
5099
  map :desc, to: :description
3496
5100
  end
3497
-
3498
- # Equivalent to the JSON, YAML, and TOML mappings.
3499
- #
3500
- # json and yaml and toml do
3501
- # map :id, to: color
3502
- # map :name, to: :full_name
3503
- # map :status, to: :current_status
3504
- # end
3505
5101
  end
3506
5102
  ----
3507
5103
 
@@ -3532,12 +5128,7 @@ desc: A ceramic with a navy blue color and clear glaze.
3532
5128
 
3533
5129
  ==== Specific format mappings
3534
5130
 
3535
- Specific key value formats can be mapping independently of other formats, including:
3536
-
3537
- * `json` for the JSON format
3538
- * `yaml` for the YAML format
3539
- * `toml` for the TOML format
3540
-
5131
+ Specific key value formats can be mapping independently of other formats.
3541
5132
 
3542
5133
  .Using the `map` method to define key-value mappings per format
3543
5134
  [example]
@@ -3548,6 +5139,11 @@ class Example < Lutaml::Model::Serializable
3548
5139
  attribute :name, :string
3549
5140
  attribute :value, :integer
3550
5141
 
5142
+ hsh do
5143
+ map 'name', to: :name
5144
+ map 'value', to: :value
5145
+ end
5146
+
3551
5147
  json do
3552
5148
  map 'name', to: :name
3553
5149
  map 'value', to: :value
@@ -3610,7 +5206,7 @@ Syntax:
3610
5206
 
3611
5207
  [source,ruby]
3612
5208
  ----
3613
- json | yaml | toml | key_value do
5209
+ hsh | json | yaml | toml | key_value do
3614
5210
  map_all to: :name_of_attribute
3615
5211
  end
3616
5212
  ----
@@ -3623,6 +5219,10 @@ end
3623
5219
  class Document < Lutaml::Model::Serializable
3624
5220
  attribute :content, :string
3625
5221
 
5222
+ hsh do
5223
+ map_all to: :content
5224
+ end
5225
+
3626
5226
  json do
3627
5227
  map_all to: :content
3628
5228
  end
@@ -3796,7 +5396,7 @@ Syntax:
3796
5396
  class SomeKeyedCollection < Lutaml::Model::Serializable
3797
5397
  attribute :name_of_attribute, AttributeValueType, collection: true
3798
5398
 
3799
- json | yaml | toml | key_value do
5399
+ hsh | json | yaml | toml | key_value do
3800
5400
  map to: :name_of_attribute, <1>
3801
5401
  root_mappings: { <2>
3802
5402
  # `:key` is a reserved keyword
@@ -3870,7 +5470,7 @@ Syntax:
3870
5470
  class SomeKeyedCollection < Lutaml::Model::Serializable
3871
5471
  attribute :name_of_attribute, AttributeValueType, collection: true
3872
5472
 
3873
- json | yaml | toml | key_value do
5473
+ hsh | json | yaml | toml | key_value do
3874
5474
  map to: :name_of_attribute,
3875
5475
  root_mappings: {
3876
5476
  value_type_attribute_name_for_key: :key, <1>
@@ -4008,7 +5608,7 @@ Syntax:
4008
5608
  class SomeKeyedCollection < Lutaml::Model::Serializable
4009
5609
  attribute :name_of_attribute, AttributeValueType, collection: true
4010
5610
 
4011
- json | yaml | toml | key_value do
5611
+ hsh | json | yaml | toml | key_value do
4012
5612
  map to: :name_of_attribute,
4013
5613
  root_mappings: {
4014
5614
  value_type_attribute_name_for_key: :key, <1>
@@ -4214,7 +5814,7 @@ Syntax:
4214
5814
  class SomeKeyedCollection < Lutaml::Model::Serializable
4215
5815
  attribute :name_of_attribute, AttributeValueType, collection: true
4216
5816
 
4217
- json | yaml | toml | key_value do
5817
+ hsh | json | yaml | toml | key_value do
4218
5818
  map to: :name_of_attribute,
4219
5819
  root_mappings: {
4220
5820
  value_type_attribute_name_for_key: :key, <1>
@@ -4378,7 +5978,7 @@ end
4378
5978
  NOTE: This feature is for key-value data model serialization only.
4379
5979
 
4380
5980
  The `child_mappings` option is used to extract results from a key-value
4381
- serialization data model (JSON, YAML, TOML) into a `Lutaml::Model::Serializable`
5981
+ serialization data model (Hash, JSON, YAML, TOML) into a `Lutaml::Model::Serializable`
4382
5982
  object (collection or not).
4383
5983
 
4384
5984
  The values are extracted from the key-value data model using the list of keys
@@ -4391,7 +5991,7 @@ Syntax:
4391
5991
  class SomeObject < Lutaml::Model::Serializable
4392
5992
  attribute :name_of_attribute, AttributeValueType, collection: true
4393
5993
 
4394
- json | yaml | toml | key_value do
5994
+ hsh | json | yaml | toml | key_value do
4395
5995
  map 'key_value_model_attribute_name', to: :name_of_attribute,
4396
5996
  child_mappings: {
4397
5997
  value_type_attribute_name_1: <1>
@@ -4556,12 +6156,353 @@ The output becomes:
4556
6156
  > #{"schemas"=>{"foo"=>{"path"=>{"link"=>"link one", "name"=>"one"}}, {"bar"=>{"path"=>{"link"=>"link two", "name"=>"two"}}}}}
4557
6157
  ----
4558
6158
 
4559
- In this example:
6159
+ In this example:
6160
+
6161
+ * The `key` of each schema (`foo` and `bar`) is mapped to the `id` attribute.
6162
+
6163
+ * The nested `path.link` and `path.name` keys are mapped to the `link` and
6164
+ `name` attributes, respectively.
6165
+ ====
6166
+
6167
+ === Collection data models
6168
+
6169
+ ==== General
6170
+
6171
+ Collection data models represent a group of models, mapping to an instance of
6172
+ Lutaml::Model::Collection.
6173
+
6174
+ Collection data models supported are identified by their short name:
6175
+
6176
+ `jsonl`:: JSONL (JSON Lines)
6177
+ `yamls`:: YAML Stream (multi-document format)
6178
+
6179
+
6180
+ ==== Mapping
6181
+
6182
+ As with collections in general, the `map` method is used to define collection
6183
+ mappings.
6184
+
6185
+ Syntax:
6186
+
6187
+ [source,ruby]
6188
+ ----
6189
+ class MySerializedCollection < Lutaml::Model::Collection
6190
+ instances {attribute}, ModelType
6191
+
6192
+ {collection_type_short} do
6193
+ map_instances to: {attribute}
6194
+ end
6195
+ end
6196
+ ----
6197
+
6198
+ Where,
6199
+
6200
+ `{collection_type_short}`:: The short name of the collection type (e.g. `jsonl`, `yamls`).
6201
+
6202
+ `{attribute}`:: The name of the attribute in the collection that will hold the
6203
+ collection data.
6204
+
6205
+ `ModelType`:: The type of the model that will be used in the collection.
6206
+
6207
+ A singular model may also utilize collection data models in the following manner.
6208
+
6209
+ Syntax:
6210
+
6211
+ [source,ruby]
6212
+ ----
6213
+ class MySerializedCollection < Lutaml::Model::Serializeable
6214
+ attribute {attribute}, ModelType, collection: true
6215
+
6216
+ {collection_type_short} do
6217
+ # Notice that there is no key_name i.e map <key_name>, to: <attribute_name>,
6218
+ # This is because in a collection there are no keys. Each object needs to be
6219
+ # mapped to the attribute.
6220
+ map to: {attribute}
6221
+ end
6222
+ end
6223
+ ----
6224
+
6225
+ Where,
6226
+
6227
+ `{collection_type_short}`:: The short name of the collection type (e.g. `jsonl`, `yamls`).
6228
+
6229
+ `{attribute}`:: The name of the attribute in the collection that will hold the
6230
+ collection data.
6231
+
6232
+ `ModelType`:: The type of the model that will be used in the collection.
6233
+
6234
+
6235
+ ==== JSONL
6236
+
6237
+ JSONL (short for JSON Lines) is a serialization format where each line
6238
+ represents a valid JSON object. The format is meant to be efficient for large
6239
+ datasets such as for streaming or batch processing.
6240
+
6241
+ It represents a collection of JSON objects encoded one object per line.
6242
+
6243
+ NOTE: The contents of JSONL itself is not valid JSON, but each line is a valid
6244
+ JSON.
6245
+
6246
+ Since JSONL contains JSON elements, the model specified with `instances` or
6247
+ `attribute` must support JSON.
6248
+
6249
+ Every line in a JSONL file is also a valid JSON object. If JSONL-specific
6250
+ mappings (through `jsonl`) are not defined in the model, the existing `json`
6251
+ mappings are used instead as a fallback for serialization and deserialization.
6252
+
6253
+
6254
+ .Parsing a JSONL collection using a collection
6255
+ [example]
6256
+ ====
6257
+ [source,ruby]
6258
+ ----
6259
+ class Person
6260
+ attribute :name, :string
6261
+ attribute :age, :integer
6262
+ attribute :id, :string
6263
+ end
6264
+
6265
+ class Directory < Lutaml::Model::Collection
6266
+ instances :persons, Person
6267
+
6268
+ jsonl do
6269
+ map_instances to: :persons
6270
+ end
6271
+ end
6272
+
6273
+ jsonl = <<~JSONL
6274
+ {"name":"John","age":30,"id":"abc-123"}
6275
+ {"name":"Jane","age":25,"id":"def-456"}
6276
+ JSONL
6277
+
6278
+ jsonl = Directory.from_jsonl(jsonl)
6279
+
6280
+ # => <Directory:0x00007fae4b0c9b10
6281
+ # @persons=[
6282
+ # <Person:0x00007fae4b0c9970 @name="John", @age=30, @id="abc-123">,
6283
+ # <Person:0x00007fae4b0c9838 @name="Jane", @age=25, @id="def-456">
6284
+ # ]>
6285
+ ----
6286
+ ====
6287
+
6288
+ .Parsing a JSONL collection using a singlular model
6289
+ [example]
6290
+ ====
6291
+ [source,ruby]
6292
+ ----
6293
+ class Person
6294
+ attribute :name, :string
6295
+ attribute :age, :integer
6296
+ attribute :id, :string
6297
+ end
6298
+
6299
+ class Directory < Lutaml::Model::Serializeable
6300
+ attribute :persons, Person, collection: true
6301
+
6302
+ jsonl do
6303
+ map_instances to: :persons
6304
+ end
6305
+ end
6306
+
6307
+ jsonl = <<~JSONL
6308
+ {"name":"John","age":30,"id":"abc-123"}
6309
+ {"name":"Jane","age":25,"id":"def-456"}
6310
+ JSONL
6311
+
6312
+ jsonl = Directory.from_jsonl(jsonl)
6313
+
6314
+ # => <Directory:0x00007fae4b0c9b10
6315
+ # @persons=[
6316
+ # <Person:0x00007fae4b0c9970 @name="John", @age=30, @id="abc-123">,
6317
+ # <Person:0x00007fae4b0c9838 @name="Jane", @age=25, @id="def-456">
6318
+ # ]>
6319
+ ----
6320
+ ====
6321
+
6322
+ .Parsing a JSONL collection relying on JSON mappings on instance model
6323
+ [example]
6324
+ ====
6325
+ [source,ruby]
6326
+ ----
6327
+ class Person
6328
+ attribute :name, :string
6329
+ attribute :age, :integer
6330
+ attribute :id, :string
6331
+
6332
+ json do
6333
+ map "full_name", to: :name
6334
+ map "age", to: :age
6335
+ map "id", to: :id
6336
+ end
6337
+ end
6338
+
6339
+ class Directory < Lutaml::Model::Collection
6340
+ instances :persons, Person
6341
+
6342
+ jsonl do
6343
+ map_instances to: :persons
6344
+ end
6345
+ end
6346
+
6347
+ jsonl = <<~JSONL
6348
+ {"full_name":"John Doe","age":30,"id":"abc-123"}
6349
+ {"full_name":"Jane Smith","age":25,"id":"def-456"}
6350
+ JSONL
6351
+
6352
+ jsonl = Directory.from_jsonl(jsonl)
6353
+ # => <Directory:0x00007fae4b0c9b10
6354
+ # @persons=[
6355
+ # <Person:0x00007fae4b0c9970 @name="John Doe", @age=30, @id="abc-123">,
6356
+ # <Person:0x00007fae4b0c9838 @name="Jane Smith", @age=25, @id="def-456">
6357
+ # ]>
6358
+ ----
6359
+ ====
6360
+
6361
+
6362
+ ==== YAML Stream
6363
+
6364
+ YAML Stream (short for YAML multi-document format) is a serialization format
6365
+ where each document is separated by a document separator (`---`). The format is
6366
+ meant to be efficient for large datasets such as for streaming or batch
6367
+ processing.
6368
+
6369
+ It represents a collection of YAML documents encoded one document per stream.
6370
+
6371
+ NOTE: The contents of YAML Stream is valid YAML, where each document is a valid
6372
+ YAML document separated by document separators.
6373
+
6374
+ Since YAML Stream contains YAML elements, the model specified with `instances`
6375
+ or `attribute` must support YAML.
6376
+
6377
+ Every document in a YAML Stream file is also a valid YAML document. If YAML
6378
+ Stream-specific mappings (through `yamls`) are not defined in the model, the
6379
+ existing `yaml` mappings are used instead as a fallback for serialization and
6380
+ deserialization.
6381
+
6382
+
6383
+ .Parsing a YAML Stream collection using a collection
6384
+ [example]
6385
+ ====
6386
+ [source,ruby]
6387
+ ----
6388
+ class Person
6389
+ attribute :name, :string
6390
+ attribute :age, :integer
6391
+ attribute :id, :string
6392
+ end
6393
+
6394
+ class Directory < Lutaml::Model::Collection
6395
+ instances :persons, Person
6396
+
6397
+ yamls do
6398
+ map_instances to: :persons
6399
+ end
6400
+ end
6401
+
6402
+ yamls = <<~YAMLS
6403
+ ---
6404
+ name: John
6405
+ age: 30
6406
+ id: abc-123
6407
+ ---
6408
+ name: Jane
6409
+ age: 25
6410
+ id: def-456
6411
+ YAMLS
6412
+
6413
+ yamls = Directory.from_yamls(yamls)
6414
+
6415
+ # => <Directory:0x00007fae4b0c9b10
6416
+ # @persons=[
6417
+ # <Person:0x00007fae4b0c9970 @name="John", @age=30, @id="abc-123">,
6418
+ # <Person:0x00007fae4b0c9838 @name="Jane", @age=25, @id="def-456">
6419
+ # ]>
6420
+ ----
6421
+ ====
6422
+
6423
+ .Parsing a YAML Stream collection using a singlular model
6424
+ [example]
6425
+ ====
6426
+ [source,ruby]
6427
+ ----
6428
+ class Person
6429
+ attribute :name, :string
6430
+ attribute :age, :integer
6431
+ attribute :id, :string
6432
+ end
6433
+
6434
+ class Directory < Lutaml::Model::Serializeable
6435
+ attribute :persons, Person, collection: true
6436
+
6437
+ yamls do
6438
+ map_instances to: :persons
6439
+ end
6440
+ end
6441
+
6442
+ yamls = <<~YAMLS
6443
+ ---
6444
+ name: John
6445
+ age: 30
6446
+ id: abc-123
6447
+ ---
6448
+ name: Jane
6449
+ age: 25
6450
+ id: def-456
6451
+ YAMLS
6452
+
6453
+ yamls = Directory.from_yamls(yamls)
6454
+
6455
+ # => <Directory:0x00007fae4b0c9b10
6456
+ # @persons=[
6457
+ # <Person:0x00007fae4b0c9970 @name="John", @age=30, @id="abc-123">,
6458
+ # <Person:0x00007fae4b0c9838 @name="Jane", @age=25, @id="def-456">
6459
+ # ]>
6460
+ ----
6461
+ ====
6462
+
6463
+ .Parsing a YAML Stream collection relying on YAML mappings on instance model
6464
+ [example]
6465
+ ====
6466
+ [source,ruby]
6467
+ ----
6468
+ class Person
6469
+ attribute :name, :string
6470
+ attribute :age, :integer
6471
+ attribute :id, :string
6472
+
6473
+ yaml do
6474
+ map "full_name", to: :name
6475
+ map "age", to: :age
6476
+ map "id", to: :id
6477
+ end
6478
+ end
4560
6479
 
4561
- * The `key` of each schema (`foo` and `bar`) is mapped to the `id` attribute.
6480
+ class Directory < Lutaml::Model::Collection
6481
+ instances :persons, Person
4562
6482
 
4563
- * The nested `path.link` and `path.name` keys are mapped to the `link` and
4564
- `name` attributes, respectively.
6483
+ yamls do
6484
+ map_instances to: :persons
6485
+ end
6486
+ end
6487
+
6488
+ yamls = <<~YAMLS
6489
+ ---
6490
+ full_name: John Doe
6491
+ age: 30
6492
+ id: abc-123
6493
+ ---
6494
+ full_name: Jane Smith
6495
+ age: 25
6496
+ id: def-456
6497
+ YAMLS
6498
+
6499
+ yamls = Directory.from_yamls(yamls)
6500
+ # => <Directory:0x00007fae4b0c9b10
6501
+ # @persons=[
6502
+ # <Person:0x00007fae4b0c9970 @name="John Doe", @age=30, @id="abc-123">,
6503
+ # <Person:0x00007fae4b0c9838 @name="Jane Smith", @age=25, @id="def-456">
6504
+ # ]>
6505
+ ----
4565
6506
  ====
4566
6507
 
4567
6508
  === Format-independent mechanisms
@@ -4639,7 +6580,7 @@ being retrieved from the model.
4639
6580
  Attribute-level `import`:: The transformation `Proc` for the value when it is
4640
6581
  being assigned to the model.
4641
6582
 
4642
- `{key_value_formats}`:: The serialization format (e.g. `json`, `yaml`, `toml`,
6583
+ `{key_value_formats}`:: The serialization format (e.g. `hsh`, `json`, `yaml`, `toml`,
4643
6584
  `key_value`) for which the mapping is defined.
4644
6585
 
4645
6586
  Mapping-level `transform`:: The option to define a transformation for the
@@ -4738,18 +6679,18 @@ format-specific custom methods.
4738
6679
  ╔════════════════════════════╗ ╔════════════════════════════╗
4739
6680
  ║ Serialization Format Value ║ ║ Serialization Format Value ║
4740
6681
  ╚════════════════════════════╝ ╚════════════════════════════╝
4741
- | |
4742
- ▼ ▼
6682
+ | ▲
6683
+ ▼ |
4743
6684
  ╔════════════════════════════╗ ╔════════════════════════════╗
4744
6685
  ║ Mapping Transform ║ ║ Mapping Transform ║
4745
6686
  ╚════════════════════════════╝ ╚════════════════════════════╝
4746
- | |
4747
- ▼ ▼
6687
+ | ▲
6688
+ ▼ |
4748
6689
  ╔════════════════════════════╗ ╔════════════════════════════╗
4749
6690
  ║ Attribute Transform ║ ║ Attribute Transform ║
4750
6691
  ╚════════════════════════════╝ ╚════════════════════════════╝
4751
- | |
4752
- ▼ ▼
6692
+ | ▲
6693
+ ▼ |
4753
6694
  ╔════════════════════════════╗ ╔════════════════════════════╗
4754
6695
  ║ Model Attribute Value ║ ║ Model Attribute Value ║
4755
6696
  ╚════════════════════════════╝ ╚════════════════════════════╝
@@ -4994,7 +6935,7 @@ xml do
4994
6935
  map_attribute 'name_of_attribute', to: :name_of_attribute, render_default: true
4995
6936
  end
4996
6937
 
4997
- json | yaml | toml | key_value do
6938
+ hsh | json | yaml | toml | key_value do
4998
6939
  map 'name_of_attribute', to: :name_of_attribute, render_default: true
4999
6940
  end
5000
6941
  ----
@@ -5071,7 +7012,7 @@ Syntax:
5071
7012
 
5072
7013
  [source,ruby]
5073
7014
  ----
5074
- json | yaml | toml | key_value do
7015
+ hsh | json | yaml | toml | key_value do
5075
7016
  map ["name1", "name2"], to: :attribute_name
5076
7017
  end
5077
7018
 
@@ -5185,7 +7126,7 @@ Syntax:
5185
7126
 
5186
7127
  [source,ruby]
5187
7128
  ----
5188
- xml | json | yaml | toml do
7129
+ xml | hsh | json | yaml | toml do
5189
7130
  map 'key_value_model_attribute_name', to: :name_of_attribute, delegate: :model_to_delegate_to
5190
7131
  end
5191
7132
  ----
@@ -5466,7 +7407,7 @@ NOTE: For `NokogiriAdapter`, we can also call `to_xml` on `value.node.adapter_no
5466
7407
  .Key-value data model serialization with custom methods
5467
7408
  [source,ruby]
5468
7409
  ----
5469
- json | yaml | toml do
7410
+ hsh | json | yaml | toml do
5470
7411
  map 'attribute_name', to: :name_of_attribute, with: {
5471
7412
  to: :method_name_to_serialize,
5472
7413
  from: :method_name_to_deserialize
@@ -5533,12 +7474,122 @@ the undefined value:: the value is not defined
5533
7474
  There are also different ways to represent these missing values
5534
7475
  when the attribute accepts a single value or a collection of values.
5535
7476
 
7477
+ .Support of missing value types in different technologies
7478
+ |===
7479
+ | Technology | Missing value type | Realized as
7480
+
7481
+ .3+| Lutaml::Model
7482
+ | empty value | Ruby empty string (`""`)
7483
+ | non-existent value | Ruby `NilClass` (`nil`)
7484
+ | undefined value | `class Uninitialized`
7485
+
7486
+ .3+| XML element
7487
+ | empty value | XML blank element: `<status></status>` or `<status/>`
7488
+ | non-existent value | XML blank element with attribute `xsi:nil`: `<status xsi:nil="true"/>`
7489
+ | undefined value | the XML element is omitted
7490
+
7491
+ .3+| XML attribute
7492
+ | empty value | XML blank attribute: `status=""`
7493
+ | non-existent value | the XML attribute is omitted
7494
+ | undefined value | the XML attribute is omitted
7495
+
7496
+ .3+| JSON
7497
+ | empty value | JSON empty string (`""`)
7498
+ | non-existent value | JSON `null` value
7499
+ | undefined value | the JSON key is omitted
7500
+
7501
+ .3+| TOML
7502
+ | empty value | TOML empty string
7503
+ | non-existent value | the TOML key is omitted since TOML does not support the concept of null.
7504
+ | undefined value | the TOML key is omitted
7505
+
7506
+ |===
7507
+
7508
+ NOTE: The `Uninitialized` class is a special Lutaml::Model construct, it is not
7509
+ supported by normal Ruby objects.
7510
+
7511
+ The challenge for the developer is how to represent fully compatible semantics
7512
+ using interoperable data models across different technologies.
7513
+
7514
+ Lutaml::Model provides you with several mechanisms to retain the missing values
7515
+ semantics. An example mapping is shown in the following diagram.
7516
+
7517
+ .Mapping of missing value types between Lutaml::Model and YAML
7518
+ [source]
7519
+ ----
7520
+ ┌─────────────────────────────────────────────────────────────────────────────┐
7521
+ │ ╔════════════════════════╗ ╔════════════════════════╗ │
7522
+ │ ║ Lutaml::Model Values ║ ║ YAML Values ║ │
7523
+ │ ╠════════════════════════╣ ╠════════════════════════╣ │
7524
+ │ ║ ║ mapping ║ ║ │
7525
+ │ ║ "empty" ║◀──────────────────▶║ "empty" ║ │
7526
+ │ ║ (empty string, []) ║ to empty ║ (empty string, []) ║ │
7527
+ │ ║ ║ ║ ║ │
7528
+ │ ╟────────────────────────╢ ╟────────────────────────╢ │
7529
+ │ ║ ║ mapping ║ ║ │
7530
+ │ ║ "non-existent" ║◀──────────────────▶║ "non-existent" ║ │
7531
+ │ ║ (nil) ║ to non-existent ║ (null) ║ │
7532
+ │ ║ ║ ║ ║ │
7533
+ │ ╟────────────────────────╢ ╟────────────────────────╢ │
7534
+ │ ║ ║ mapping ║ ║ │
7535
+ │ ║ "undefined" ║◀──────────────────▶║ "undefined" ║ │
7536
+ │ ║ (uninitialized) ║ to undefined ║ (key omitted) ║ │
7537
+ │ ║ ║ ║ ║ │
7538
+ │ ╚════════════════════════╝ ╚════════════════════════╝ │
7539
+ └─────────────────────────────────────────────────────────────────────────────┘
7540
+ ----
7541
+
7542
+ In the case where the interoperating technologies do not support the full spectrum
7543
+ of missing value types, it is necessary for the developer to understand any
7544
+ such behavior and relevant handling.
7545
+
7546
+ .Mapping of missing value types between Lutaml::Model and TOML
7547
+ [source]
7548
+ ----
7549
+ ┌─────────────────────────────────────────────────────────────────────────────┐
7550
+ │ ╔════════════════════════╗ ╔════════════════════════╗ │
7551
+ │ ║ Lutaml::Model Values ║ ║ TOML Values ║ │
7552
+ │ ╠════════════════════════╣ ╠════════════════════════╣ │
7553
+ │ ║ ║ mapping ║ ║ │
7554
+ │ ║ "empty" ║◀──────────────────▶║ "empty" ║ │
7555
+ │ ║ (empty string, []) ║ to empty ║ (empty string, []) ║ │
7556
+ │ ║ ║ ║ ║ │
7557
+ │ ╟────────────────────────╢ ╟────────────────────────╢ │
7558
+ │ ║ ║ mapping ║ ║ │
7559
+ │ ║ "non-existent" ║◀──────────────────▶║ ║ │
7560
+ │ ║ (nil) ║ to undefined ║ "undefined" ║ │
7561
+ │ ║ ║ ║ (key omitted) ║ │
7562
+ │ ╟────────────────────────╢ ║ ║ │
7563
+ │ ║ ║ mapping ║ ║ │
7564
+ │ ║ "undefined" ║─────(one-way)─────▶║ TOML does not ║ │
7565
+ │ ║ (uninitialized) ║ to undefined ║ support NULL ║ │
7566
+ │ ║ ║ ║ ║ │
7567
+ │ ╚════════════════════════╝ ╚════════════════════════╝ │
7568
+ └─────────────────────────────────────────────────────────────────────────────┘
7569
+ ----
7570
+
7571
+ There are the following additional challenges that a developer must take into account of:
7572
+
7573
+ * Single attribute value vs collection attribute value. Different technologies
7574
+ treat single/collection values differently.
7575
+
7576
+ * External schemas and systems that interoperate with serializations from
7577
+ Lutaml::Model. Many schemas and systems adopt "different" conventions for
7578
+ representing missing value semantics (sometimes very awkward ones).
7579
+
7580
+ The solution for the first challenge is to understand the behavior of the
7581
+ different technologies used. The default mappings are described in <<value_representation_in_lutaml-model>>
7582
+ and <<value_representation_in_serialization_formats>>.
7583
+
7584
+
7585
+ [[value_representation_in_lutaml-model]]
5536
7586
  ==== Value representation in Lutaml::Model
5537
7587
 
5538
7588
  The following table summarizes the behavior of the Lutaml::Model
5539
7589
  in regards of the "missing values" family.
5540
7590
 
5541
- [cols="1,1,1,1", options="header"]
7591
+ .Handling of missing value types in Lutaml::Model data types
7592
+ [cols="1,1,2,2"]
5542
7593
  |===
5543
7594
  | LutaML value type | Cardinality (1 or n) | Missing value type | Ruby value
5544
7595
 
@@ -5556,55 +7607,49 @@ in regards of the "missing values" family.
5556
7607
 
5557
7608
  .3+| `:integer`
5558
7609
  .3+| single
5559
- | empty value | `0` (Integer)
7610
+ | empty value | N/A
5560
7611
  | non-existent value | `nil` (NilClass)
5561
7612
  | undefined value | No assigned value
5562
7613
 
5563
7614
  .3+| `:float`
5564
7615
  .3+| single
5565
- | empty value | `0.0` (Float)
7616
+ | empty value | N/A
5566
7617
  | non-existent value | `nil` (NilClass)
5567
7618
  | undefined value | No assigned value
5568
7619
 
5569
7620
  .3+| `:boolean`
5570
7621
  .3+| single
5571
- | empty value | `false` (FalseClass)
5572
- | non-existent value | `nil` (NilClass)
5573
- | undefined value | No assigned value
5574
-
5575
- .3+| `:date`
5576
- .3+| single
5577
- | empty value | `Date.new(0)` (Date)
7622
+ | empty value | N/A
5578
7623
  | non-existent value | `nil` (NilClass)
5579
7624
  | undefined value | No assigned value
5580
7625
 
5581
7626
  .3+| `:date`
5582
7627
  .3+| single
5583
- | empty value | `Date.new(0)` (Date)
7628
+ | empty value | N/A
5584
7629
  | non-existent value | `nil` (NilClass)
5585
7630
  | undefined value | No assigned value
5586
7631
 
5587
7632
  .3+| `:time_without_date`
5588
7633
  .3+| single
5589
- | empty value | `Time.new(0)` (Time)
7634
+ | empty value | N/A
5590
7635
  | non-existent value | `nil` (NilClass)
5591
7636
  | undefined value | No assigned value
5592
7637
 
5593
7638
  .3+| `:date_time`
5594
7639
  .3+| single
5595
- | empty value | `Time.new(0)` (Time)
7640
+ | empty value | N/A
5596
7641
  | non-existent value | `nil` (NilClass)
5597
7642
  | undefined value | No assigned value
5598
7643
 
5599
7644
  .3+| `:time`
5600
7645
  .3+| single
5601
- | empty value | `Time.new(0)` (Time)
7646
+ | empty value | N/A
5602
7647
  | non-existent value | `nil` (NilClass)
5603
7648
  | undefined value | No assigned value
5604
7649
 
5605
7650
  .3+| `:decimal`
5606
7651
  .3+| single
5607
- | empty value | `BigDecimal.new(0)` (BigDecimal)
7652
+ | empty value | N/A
5608
7653
  | non-existent value | `nil` (NilClass)
5609
7654
  | undefined value | No assigned value
5610
7655
 
@@ -5617,6 +7662,7 @@ in regards of the "missing values" family.
5617
7662
  |===
5618
7663
 
5619
7664
 
7665
+ [[value_representation_in_serialization_formats]]
5620
7666
  ==== Value representation in serialization formats
5621
7667
 
5622
7668
  Every serialization format uses a different information model to represent
@@ -5674,6 +7720,8 @@ while others only support a subset of them.
5674
7720
 
5675
7721
  ==== Missing value mapping
5676
7722
 
7723
+ ===== General
7724
+
5677
7725
  Lutaml::Model provides a comprehensive way to handle the missing values family
5678
7726
  across different serialization formats.
5679
7727
 
@@ -5689,7 +7737,8 @@ A hash of key-value pairs that determines the mapping of a missing value at the
5689
7737
  The key is the missing value type in the serialization format, and the value is
5690
7738
  the missing value type in the LutaML Model.
5691
7739
  +
5692
- NOTE: In other words, used when converting the serialized format into a Ruby object.
7740
+ NOTE: In other words, used when converting the serialized format into a
7741
+ Lutaml::Model Ruby object.
5693
7742
 
5694
7743
  `to` pairs::
5695
7744
  A hash of key-value pairs that determines the mapping of a LutaML Model ("to") missing
@@ -5697,7 +7746,8 @@ to a missing value choice at the serialization format where this mapping applies
5697
7746
  The key is the missing value type in the LutaML Model, and the value is the
5698
7747
  missing value type in the serialization format.
5699
7748
  +
5700
- NOTE: In other words, used when converting Ruby objects back into the serialized format.
7749
+ NOTE: In other words, used when converting a Lutaml::Model Ruby object into the
7750
+ serialized format.
5701
7751
 
5702
7752
  Syntax:
5703
7753
 
@@ -5705,101 +7755,616 @@ Syntax:
5705
7755
  ----
5706
7756
  {map_command} 'format-key', to: :attribute_name, value_map: { <1>
5707
7757
  from: {
5708
- {format-missing-value-1}: {model-missing-value-1}, <2>
5709
- {format-missing-value-2}: {model-missing-value-2},
5710
- {format-missing-value-3}: {model-missing-value-3}
7758
+ {format-missing-value-n}: {model-missing-value-n}, <2>
7759
+ {format-missing-value-m}: {model-missing-value-m},
7760
+ {format-missing-value-o}: {model-missing-value-o}
5711
7761
  },
5712
7762
  to: {
5713
- {model-missing-value-1}: {format-missing-value-1}, <3>
5714
- {model-missing-value-2}: {format-missing-value-2},
5715
- {model-missing-value-3}: {format-missing-value-3}
7763
+ {model-missing-value-n}: {format-missing-value-n}, <3>
7764
+ {model-missing-value-m}: {format-missing-value-m},
7765
+ {model-missing-value-o}: {format-missing-value-o}
7766
+ }
7767
+ }
7768
+ ----
7769
+ <1> The `{map_command}` is a mapping rule with the actual command depending on
7770
+ the serialization format.
7771
+ <2> In the `from` mapping, the keys are the missing value types in the
7772
+ serialization format.
7773
+ <3> In the `to` mapping, the keys are the missing value types in the LutaML
7774
+ Model.
7775
+
7776
+ The missing value type mapping differs per serialization format,
7777
+ as serialization formats may not fully support all missing value types.
7778
+
7779
+ The availability of `from` and `to` keys and values depend on the types of
7780
+ missing values supported by that particular serialization format.
7781
+
7782
+ The available values for `from` and `to` for serialization formats
7783
+ are presented below, where the allowed values are to be used in the direction
7784
+ of the format. That means if the format supports `:empty`, it can be used
7785
+ as a key in `from:` direction, and the value in the `to:` direction (see `{format-missing-value-n}`) in the syntax.
7786
+
7787
+ .Available missing value types in different mapping commands
7788
+ [cols="a,2a"]
7789
+ |===
7790
+ | Map command | Missing value types available (key in `from:` direction, value in the `to:` direction)
7791
+
7792
+ | XML element `map_element` | `:empty`, `:omitted`, `:nil`
7793
+ | XML attribute `map_attribute` | `:empty`, `:omitted`
7794
+ | Hash `map`, `map_content` | `:empty`, `:omitted`, `:nil`
7795
+ | JSON `map`, `map_content` | `:empty`, `:omitted`, `:nil`
7796
+ | YAML `map`, `map_content` | `:empty`, `:omitted`, `:nil`
7797
+ | TOML `map`, `map_content` | `:empty`, `:omitted`
7798
+
7799
+ |===
7800
+
7801
+
7802
+ [example]
7803
+ For instance, TOML does not support the notion of "null" and therefore the
7804
+ missing value type of `nil` cannot be used; therefore in a `from:`
7805
+ value map, it is not possible to indicate `nil: {model-missing-value}`.
7806
+
7807
+ [example]
7808
+ ====
7809
+ In an XML mapping block, it is possible to do the following.
7810
+
7811
+ [source,ruby]
7812
+ ----
7813
+ xml do
7814
+ map_element 'status', to: :status, value_map: {
7815
+ from: { empty: :nil, omitted: :omitted, nil: :nil },
7816
+ to: { empty: :nil, omitted: :omitted, nil: :nil }
7817
+ }
7818
+ end
7819
+ ----
7820
+ ====
7821
+
7822
+ Each serialization format has specific behavior when handling values
7823
+ such as `empty`, `omitted`, and `nil`.
7824
+
7825
+ Users can specify the mapping for both `from` and `to` values using the
7826
+ `value_map` option in the attribute definition.
7827
+
7828
+ * The keys that can be used in the `from` and `to` mappings are `empty`,
7829
+ `omitted`, and `nil`.
7830
+
7831
+ * The values in the mappings can also be `empty`, `omitted`, and `nil`.
7832
+
7833
+ NOTE: Since `nil` is not supported in TOML, so mappings like `nil:
7834
+ {any_option}` or `{any_option}: :nil` will not work in TOML.
7835
+
7836
+ NOTE: In a collection attribute, the values of `value_map` also depend on the
7837
+ `initialize_empty` setting, where an omitted value in the serialization format can still lead to a `nil`
7838
+ or an empty array `[]` at the attribute-level (instead of the mapping-level).
7839
+
7840
+
7841
+
7842
+ ===== Default value maps for serialization formats
7843
+
7844
+ The table below describes the default `value_map` configurations for supported
7845
+ serialization formats.
7846
+
7847
+ // TODO: Find a place to write this
7848
+ // XML Handling::
7849
+ // - `<status>new</status>` will be treated as `["new"]`.
7850
+ // - `<status>new</status><status>assigned</status>` will be treated as `["new", "assigned"]`.
7851
+
7852
+ ====== Default value map for XML element (single attribute)
7853
+
7854
+ [source,ruby]
7855
+ ----
7856
+ attribute :attr, :string
7857
+
7858
+ xml do
7859
+ map_element 'key', to: :attr, value_map: {
7860
+ from: { empty: :nil, omitted: :omitted, nil: :nil },
7861
+ to: { empty: :empty, omitted: :omitted, nil: :nil }
7862
+ }
7863
+ end
7864
+ ----
7865
+
7866
+ .Default missing value mapping configuration for single attributes in XML elements
7867
+ [cols="a,a,a,a"]
7868
+ |===
7869
+
7870
+ h| Direction h| Map rule h| XML source h| Model target
7871
+
7872
+ .3+|`from:`
7873
+ | `empty: :nil`
7874
+ | blank XML element (`<status/>`)
7875
+ | `nil`
7876
+
7877
+ | `omitted: :omitted`
7878
+ | absent XML element
7879
+ | omitted from the model
7880
+
7881
+ | `nil: :nil`
7882
+ | blank XML element with attribute `xsi:nil` (`<status xsi:nil=true/>`)
7883
+ | `nil` value in the model
7884
+
7885
+
7886
+ h| Direction h| Map rule h| Model source h| XML target
7887
+
7888
+ .3+|`to:`
7889
+ | `empty: :empty`
7890
+ | empty string (`""`)
7891
+ | blank XML element
7892
+
7893
+ | `omitted: :omitted`
7894
+ | omitted in the model
7895
+ | XML element not rendered
7896
+
7897
+ | `nil: :nil`
7898
+ | `nil` value in the model
7899
+ | blank XML element with attribute `xsi:nil` (`<status xsi:nil=true/>`)
7900
+
7901
+ |===
7902
+
7903
+ ====== Default value map for XML element (collection attribute)
7904
+
7905
+ [source,ruby]
7906
+ ----
7907
+ attribute :attr, :string, collection: true
7908
+
7909
+ xml do
7910
+ map_element 'key', to: :attr, value_map: {
7911
+ from: { empty: :empty, omitted: :omitted, nil: :nil },
7912
+ to: { empty: :empty, omitted: :omitted, nil: :nil }
7913
+ }
7914
+ end
7915
+ ----
7916
+
7917
+ .Default missing value mapping configuration for collection attributes in XML elements
7918
+ [cols="a,a,a,a"]
7919
+ |===
7920
+
7921
+ h| Direction h| Map rule h| XML source h| Model target
7922
+
7923
+ .3+|`from:`
7924
+ | `empty: :nil`
7925
+ | blank XML element (`<status/>`)
7926
+ | empty array (`[]`)
7927
+
7928
+ | `omitted: :omitted`
7929
+ | absent XML element
7930
+ | omitted from the model
7931
+
7932
+ | `nil: :nil`
7933
+ | blank XML element with attribute `xsi:nil` (`<status xsi:nil=true/>`)
7934
+ | `nil` value in the model
7935
+
7936
+
7937
+ h| Direction h| Map rule h| Model source h| XML target
7938
+
7939
+ .3+|`to:`
7940
+ | `empty: :empty`
7941
+ | empty array (`[]`)
7942
+ | blank XML element
7943
+
7944
+ | `omitted: :omitted`
7945
+ | omitted in the model
7946
+ | XML element not rendered
7947
+
7948
+ | `nil: :nil`
7949
+ | `nil` value in the model
7950
+ | blank XML element with attribute `xsi:nil` (`<status xsi:nil=true/>`)
7951
+
7952
+ |===
7953
+
7954
+ ====== Default value map for XML attribute (single attribute)
7955
+
7956
+ [source,ruby]
7957
+ ----
7958
+ attribute :attr, :string
7959
+
7960
+ xml do
7961
+ map_attribute 'attr_name', to: :attr, value_map: {
7962
+ from: { empty: :nil, omitted: :omitted },
7963
+ to: { empty: :empty, nil: :empty, omitted: :omitted }
7964
+ }
7965
+ end
7966
+ ----
7967
+
7968
+ .Default missing value mapping configuration for single attributes in XML attributes
7969
+ [cols="a,a,a,a"]
7970
+ |===
7971
+
7972
+ h| Direction h| Map rule h| XML source h| Model target
7973
+
7974
+ .2+|`from:` (source only supports `empty` and `omitted`)
7975
+ | `empty: :nil`
7976
+ | blank XML attribute (`status=""`)
7977
+ | `nil`
7978
+
7979
+ | `omitted: :omitted`
7980
+ | absent XML attribute
7981
+ | omitted from the model
7982
+
7983
+ h| Direction h| Map rule h| Model source h| XML target
7984
+
7985
+ .3+|`to:` (target only accepts `empty` and `omitted`)
7986
+ | `empty: :empty`
7987
+ | empty string (`""`)
7988
+ | blank XML attribute (`status=""`)
7989
+
7990
+ | `omitted: :omitted`
7991
+ | omitted in the model
7992
+ | XML attribute not rendered
7993
+
7994
+ | `nil: :empty`
7995
+ | `nil`
7996
+ | blank XML attribute (`status=""`)
7997
+
7998
+ |===
7999
+
8000
+ ====== Default value map for XML attribute (collection attribute)
8001
+
8002
+ [source,ruby]
8003
+ ----
8004
+ attribute :attr, :string, collection: true
8005
+
8006
+ xml do
8007
+ map_attribute 'attr_name', to: :attr, value_map: {
8008
+ from: { empty: :empty, omitted: :omitted },
8009
+ to: { empty: :empty, nil: :omitted, omitted: :omitted }
8010
+ }
8011
+ end
8012
+ ----
8013
+
8014
+ .Default missing value mapping configuration for collection attributes in XML attributes
8015
+ [cols="a,a,a,a"]
8016
+ |===
8017
+
8018
+ h| Direction h| Map rule h| XML source h| Model target
8019
+
8020
+ .2+|`from:` (source only supports `empty` and `omitted`)
8021
+ | `empty: :empty`
8022
+ | blank XML attribute (`status=""`)
8023
+ | empty array (`[]`)
8024
+
8025
+ | `omitted: :omitted`
8026
+ | absent XML attribute
8027
+ | omitted from the model
8028
+
8029
+ h| Direction h| Map rule h| Model source h| XML target
8030
+
8031
+ .3+|`to:` (target only accepts `empty` and `omitted`)
8032
+ | `empty: :empty`
8033
+ | empty array (`[]`)
8034
+ | blank XML attribute (`status=""`)
8035
+
8036
+ | `omitted: :omitted`
8037
+ | omitted in the model
8038
+ | XML attribute not rendered
8039
+
8040
+ | `nil: :omitted`
8041
+ | `nil`
8042
+ | XML attribute not rendered
8043
+
8044
+ |===
8045
+
8046
+
8047
+
8048
+ ====== Default value map for YAML (single attribute)
8049
+
8050
+ [source,ruby]
8051
+ ----
8052
+ attribute :attr, :string
8053
+
8054
+ yaml do
8055
+ map 'key', to: :attr, value_map: {
8056
+ from: { empty: :empty, omitted: :omitted, nil: :nil },
8057
+ to: { empty: :empty, omitted: :omitted, nil: :nil }
8058
+ }
8059
+ end
8060
+ ----
8061
+
8062
+ .Default missing value mapping configuration for single attributes in YAML
8063
+ [cols="a,a,a,a"]
8064
+ |===
8065
+
8066
+ h| Direction h| Map rule h| XML source h| Model target
8067
+
8068
+ .3+|`from:`
8069
+ | `empty: :empty`
8070
+ | empty string in YAML (`status:` or `status: ""`)
8071
+ | empty string (`""`)
8072
+
8073
+ | `omitted: :omitted`
8074
+ | absent YAML key
8075
+ | omitted from the model
8076
+
8077
+ | `nil: :nil`
8078
+ | YAML `null`
8079
+ | `nil`
8080
+
8081
+ h| Direction h| Map rule h| Model source h| XML target
8082
+
8083
+ .3+|`to:`
8084
+ | `empty: :empty`
8085
+ | empty string (`""`)
8086
+ | empty string in YAML (`status:`)
8087
+
8088
+ | `omitted: :omitted`
8089
+ | omitted in the model
8090
+ | YAML key omitted
8091
+
8092
+ | `nil: :nil`
8093
+ | `nil`
8094
+ | YAML `null`
8095
+
8096
+ |===
8097
+
8098
+ NOTE: In order to treat a YAML value like `status: ''` to `nil`, the mapping of
8099
+ `value_map: { from: { empty: :nil } }` can be applied.
8100
+
8101
+
8102
+ ====== Default value map for YAML (collection attribute)
8103
+
8104
+ [source,ruby]
8105
+ ----
8106
+ attribute :attr, :string, collection: true
8107
+
8108
+ yaml do
8109
+ map 'key', to: :attr, value_map: {
8110
+ from: { empty: :empty, omitted: :omitted, nil: :nil },
8111
+ to: { empty: :empty, omitted: :omitted, nil: :nil }
8112
+ }
8113
+ end
8114
+ ----
8115
+
8116
+ .Default missing value mapping configuration for collection attributes in YAML
8117
+ [cols="a,a,a,a"]
8118
+ |===
8119
+
8120
+ h| Direction h| Map rule h| YAML source h| Model target
8121
+
8122
+ .3+|`from:`
8123
+ | `empty: :empty`
8124
+ | empty YAML array (`status:` or `status: []`)
8125
+ | empty array (`[]`)
8126
+
8127
+ | `omitted: :omitted`
8128
+ | absent YAML key
8129
+ | omitted from the model
8130
+
8131
+ | `nil: :nil`
8132
+ | YAML `null`
8133
+ | `nil`
8134
+
8135
+ h| Direction h| Map rule h| Model source h| YAML target
8136
+
8137
+ .3+|`to:`
8138
+ | `empty: :empty`
8139
+ | empty array (`[]`)
8140
+ | empty YAML array (`status: []`)
8141
+
8142
+ | `omitted: :omitted`
8143
+ | omitted in the model
8144
+ | YAML key omitted
8145
+
8146
+ | `nil: :nil`
8147
+ | `nil`
8148
+ | YAML `null`
8149
+
8150
+ |===
8151
+
8152
+ NOTE: If the YAML key for the collection attribute is omitted, it will be treated
8153
+ as `nil` or an empty array depending on the `initialize_empty` setting.
8154
+
8155
+
8156
+ ====== Default value map for JSON (single attribute)
8157
+
8158
+ [source,ruby]
8159
+ ----
8160
+ attribute :attr, :string
8161
+
8162
+ json do
8163
+ map 'key', to: :attr, value_map: {
8164
+ from: { empty: :empty, omitted: :omitted, nil: :nil },
8165
+ to: { empty: :empty, omitted: :omitted, nil: :nil }
8166
+ }
8167
+ end
8168
+ ----
8169
+
8170
+ .Default missing value mapping configuration for single attributes in JSON
8171
+ [cols="a,a,a,a"]
8172
+ |===
8173
+
8174
+ h| Direction h| Map rule h| JSON source h| Model target
8175
+
8176
+ .3+|`from:`
8177
+ | `empty: :empty`
8178
+ | empty string in JSON (`"status" : ""`)
8179
+ | empty string (`""`)
8180
+
8181
+ | `omitted: :omitted`
8182
+ | absent JSON key
8183
+ | omitted from the model
8184
+
8185
+ | `nil: :nil`
8186
+ | JSON `null`
8187
+ | `nil`
8188
+
8189
+ h| Direction h| Map rule h| Model source h| JSON target
8190
+
8191
+ .3+|`to:`
8192
+ | `empty: :empty`
8193
+ | empty string (`""`)
8194
+ | empty string in JSON (`"status" : ""`)
8195
+
8196
+ | `omitted: :omitted`
8197
+ | omitted in the model
8198
+ | JSON key omitted
8199
+
8200
+ | `nil: :nil`
8201
+ | `nil`
8202
+ | JSON `null`
8203
+
8204
+ |===
8205
+
8206
+
8207
+
8208
+ ====== Default value map for JSON (collection attribute)
8209
+
8210
+ [source,ruby]
8211
+ ----
8212
+ attribute :attr, :string, collection: true
8213
+
8214
+ json do
8215
+ map 'key', to: :attr, value_map: {
8216
+ from: { empty: :empty, omitted: :omitted, nil: :nil },
8217
+ to: { empty: :empty, omitted: :omitted, nil: :nil }
5716
8218
  }
5717
- }
8219
+ end
5718
8220
  ----
5719
- <1> The `{map_command}` is a mapping rule with the actual command depending on
5720
- the serialization format.
5721
- <2> In the `from` mapping, the keys are the missing value types in the
5722
- serialization format.
5723
- <3> In the `to` mapping, the keys are the missing value types in the LutaML
5724
- Model.
5725
8221
 
5726
- The missing value type mapping differs per serialization format,
5727
- as serialization formats may not fully support all missing value types.
8222
+ .Default missing value mapping configuration for collection attributes in JSON
8223
+ [cols="a,a,a,a"]
8224
+ |===
5728
8225
 
5729
- [example]
5730
- For instance, TOML does not support the notion of "null" and therefore the
5731
- missing value type of `nil` cannot be used; therefore in a `from:`
5732
- value map, it is not possible to indicate `nil: {model-missing-value}`.
8226
+ h| Direction h| Map rule h| JSON source h| Model target
5733
8227
 
5734
- [example]
5735
- ====
5736
- In an XML mapping block, it is possible to do the following.
8228
+ .3+|`from:`
8229
+ | `empty: :empty`
8230
+ | empty JSON array (`"status": []`)
8231
+ | empty array (`[]`)
8232
+
8233
+ | `omitted: :omitted`
8234
+ | absent JSON key
8235
+ | omitted from the model
8236
+
8237
+ | `nil: :nil`
8238
+ | JSON `null`
8239
+ | `nil`
8240
+
8241
+ h| Direction h| Map rule h| Model source h| JSON target
8242
+
8243
+ .3+|`to:`
8244
+ | `empty: :empty`
8245
+ | empty array (`[]`)
8246
+ | empty JSON array (`"status": []`)
8247
+
8248
+ | `omitted: :omitted`
8249
+ | omitted in the model
8250
+ | JSON key omitted
8251
+
8252
+ | `nil: :nil`
8253
+ | `nil`
8254
+ | JSON `null`
8255
+
8256
+ |===
8257
+
8258
+ ====== Default value map for TOML (single attribute)
8259
+
8260
+ TOML does not support the concept of `nil` and therefore the mapping of `from:`
8261
+ direction with `nil` to will not work in TOML.
8262
+
8263
+ The `nil` mapping is only supported in the `to:` direction (model to TOML).
5737
8264
 
5738
8265
  [source,ruby]
5739
8266
  ----
5740
- xml do
5741
- map_element 'status', to: :status, value_map: {
5742
- from: { empty: :nil, omitted: :omitted, nil: :nil },
5743
- to: { empty: :nil, omitted: :omitted, nil: :nil }
8267
+ attribute :attr, :string
8268
+
8269
+ toml do
8270
+ map 'key', to: :attr, value_map: {
8271
+ from: { empty: :empty, omitted: :omitted },
8272
+ to: { empty: :empty, omitted: :omitted, nil: :omitted }
5744
8273
  }
5745
8274
  end
5746
8275
  ----
5747
- ====
5748
8276
 
5749
- Each serialization format has specific behavior when handling values
5750
- such as `empty`, `omitted`, and `nil`.
8277
+ .Default missing value mapping configuration for single attributes in TOML
8278
+ [cols="a,a,a,a"]
8279
+ |===
5751
8280
 
5752
- Users can specify the mapping for both `from` and `to` values using the
5753
- `value_map` option in the attribute definition.
8281
+ h| Direction h| Map rule h| TOML source h| Model target
5754
8282
 
5755
- * The keys that can be used in the `from` and `to` mappings are `empty`,
5756
- `omitted`, and `nil`.
8283
+ .2+|`from:` (source only supports `empty` and `omitted`)
8284
+ | `empty: :empty`
8285
+ | empty string in TOML (`[status]` with no value)
8286
+ | empty string (`""`)
5757
8287
 
5758
- * The values in the mappings can also be `empty`, `omitted`, and `nil`.
8288
+ | `omitted: :omitted`
8289
+ | absent TOML key
8290
+ | omitted from the model
5759
8291
 
5760
- NOTE: Since `nil` is not supported in TOML, so mappings like `nil:
5761
- {any_option}` or `{any_option}: :nil` will not work in TOML.
8292
+ h| Direction h| Map rule h| Model source h| TOML target
5762
8293
 
5763
- The table below describes the default `value_map` configurations for supported
5764
- serialization formats.
8294
+ .3+|`to:` (source only supports `empty` and `omitted`)
8295
+ | `empty: :empty`
8296
+ | empty string (`""`)
8297
+ | empty string in TOML (`[status]` with no value)
8298
+
8299
+ | `omitted: :omitted`
8300
+ | omitted in the model
8301
+ | TOML key omitted
8302
+
8303
+ | `nil: :omitted`
8304
+ | `nil`
8305
+ | TOML key omitted
8306
+
8307
+ |===
8308
+
8309
+
8310
+
8311
+ ====== Default value map for TOML (collection attribute)
8312
+
8313
+ TOML does not support the concept of `nil` and therefore the mapping of `from:`
8314
+ direction with `nil` to will not work in TOML.
8315
+
8316
+ The `nil` mapping is only supported in the `to:` direction (model to TOML).
8317
+
8318
+ [source,ruby]
8319
+ ----
8320
+ attribute :attr, :string, collection: true
8321
+
8322
+ toml do
8323
+ map 'key', to: :attr, value_map: {
8324
+ from: { empty: :empty, omitted: :omitted },
8325
+ to: { empty: :empty, omitted: :omitted, nil: :omitted }
8326
+ }
8327
+ end
8328
+ ----
5765
8329
 
5766
- .Default missing value mapping configuration for single attribute to serialization formats
5767
- [cols="1,1,1,1", options="header"]
8330
+ .Default missing value mapping configuration for collection attributes in TOML
8331
+ [cols="a,a,a,a"]
5768
8332
  |===
5769
- | Serialization format | `from` value | `to` value | Description
5770
8333
 
5771
- .3+| XML (supports all 3 types)
5772
- | `empty` | `nil` |
5773
- * Element: An empty value in the model is treated as an element with attribute `xsi:nil` during serialization.
5774
- * Attribute: An empty value in the model is treated as an empty string in the attribute.
8334
+ h| Direction h| Map rule h| TOML source h| Model target
5775
8335
 
5776
- | `omitted` | `omitted` |
5777
- * Element: If the value is omitted in the model, the element is omitted from the XML output.
5778
- * Attribute: If the value is omitted in the model, the attribute is omitted from the XML output.
8336
+ .2+|`from:` (source only supports `empty` and `omitted`)
8337
+ | `empty: :empty`
8338
+ | empty TOML array (`[status]` with no value)
8339
+ | empty array (`[]`)
5779
8340
 
5780
- | `nil` | `nil` |
5781
- * Element: A `nil` value in the model is serialized as an element with attribute `xsi:nil`.
5782
- * Attribute: A `nil` value in the model is treated as an empty string in the attribute.
8341
+ | `omitted: :omitted`
8342
+ | absent TOML key
8343
+ | omitted from the model
5783
8344
 
5784
- .3+| YAML (supports all 3 types)
5785
- | `empty` | `empty` | An empty string (`""`) in the model is deserialized as `empty` (an empty YAML string).
5786
- | `omitted` | `omitted` | If the value is omitted in the model, the key is omitted from the YAML output.
5787
- | `nil` | `nil` | A `nil` value in the model is serialized as YAML `null`.
8345
+ h| Direction h| Map rule h| Model source h| TOML target
5788
8346
 
5789
- .3+| JSON (supports all 3 types)
5790
- | `empty` | `empty` | An empty string (`""`) in the model is serialized as `empty` (an empty JSON string).
5791
- | `omitted` | `omitted` | If the value is omitted in the model, the key is omitted from the JSON output.
5792
- | `nil` | `nil` | A `nil` value in the model is serialized as JSON `null`.
8347
+ .3+|`to:` (source only supports `empty` and `omitted`)
8348
+ | `empty: :empty`
8349
+ | empty array (`[]`)
8350
+ | empty TOML array (`[status]` with no value)
5793
8351
 
5794
- .3+| TOML (does not support `nil`)
5795
- | `empty` | `empty` | An empty string (`""`) in the model is serialized as `empty` (an empty TOML string).
5796
- | `omitted` | `omitted` | If the value is omitted in the model, the key is omitted from the TOML output.
5797
- | `nil` | `omitted` | In TOML, `nil` is represented as `omitted` due to TOML limitations.
8352
+ | `omitted: :omitted`
8353
+ | omitted in the model
8354
+ | TOML key omitted
8355
+
8356
+ | `nil: :omitted`
8357
+ | `nil`
8358
+ | TOML key omitted
5798
8359
 
5799
8360
  |===
5800
8361
 
5801
8362
 
5802
- The `value_map` option can be defined for each serialization format as follows.
8363
+
8364
+ ===== Replacing missing values type mapping with `value_map`
8365
+
8366
+ The `value_map` option can be defined to meticulously map for each serialization
8367
+ format as follows.
5803
8368
 
5804
8369
  [example]
5805
8370
  ====
@@ -5816,7 +8381,7 @@ class ExampleClass < Lutaml::Model::Serializable
5816
8381
  }
5817
8382
  end
5818
8383
 
5819
- json | yaml | toml | key_value do
8384
+ hsh | json | yaml | toml | key_value do
5820
8385
  map 'status', to: :status, value_map: {
5821
8386
  from: { empty: :nil, omitted: :omitted, nil: :nil },
5822
8387
  to: { empty: :nil, omitted: :omitted, nil: :nil }
@@ -5857,57 +8422,6 @@ When defining an attribute with `collection: true`, the attribute will behave as
5857
8422
  attribute :status, :string, collection: true
5858
8423
  ----
5859
8424
 
5860
- - **XML Handling**:
5861
- - `<status />` will be treated as `[]` (an empty array).
5862
- - `<status>new</status>` will be treated as `["new"]`.
5863
- - `<status>new</status><status>assigned</status>` will be treated as `["new", "assigned"]`.
5864
-
5865
- - **YAML Handling**:
5866
- - A value like `status: ''` in YAML will be deserialized as `nil` when `value_map: { from: { empty: :nil } }` is applied.
5867
- - If the collection is omitted, it will be initialized as `nil` or an empty array depending on the `initialize_empty` setting.
5868
-
5869
- - **JSON Handling**:
5870
- - A `status: []` in the serialized output will correspond to an empty array when `value_map: { from: { empty: :empty } }` is applied.
5871
-
5872
- - **TOML Handling**:
5873
- - TOML treats empty collections similarly to other formats, but `nil` values are not supported so we can not read or write nil values in TOML.
5874
-
5875
- .Default missing value mapping configuration for collection attributes to serialization formats
5876
- [cols="1,1,1,1", options="header"]
5877
- |===
5878
- | Serialization format | `from` value | `to` value | Description
5879
-
5880
- .3+| XML (supports all 3 types)
5881
- | `empty` | `empty` |
5882
- * Element: An empty array (`[]`) in the model is treated a blank element (`<x/>`)
5883
- * Attribute: An empty array (`[]`) in the model is treated as an empty string in the attribute.
5884
-
5885
- | `omitted` | `omitted` |
5886
- * Element: If the value is omitted in the model, the element is omitted from the XML output.
5887
- * Attribute: If the value is omitted in the model, the attribute is omitted from the XML output.
5888
-
5889
- | `nil` | `nil` |
5890
- * Element: A `nil` value in the model is serialized as an element with attribute `xsi:nil`.
5891
- * Attribute: A `nil` value in the model is treated as an empty string in the attribute.
5892
-
5893
- .3+| YAML (supports all 3 types)
5894
- | `empty` | `empty` | An empty array (`[]`) in the model is deserialized as `empty` (an empty YAML array).
5895
- | `omitted` | `omitted` | If the value is omitted in the model, the key is omitted from the YAML output.
5896
- | `nil` | `nil` | A `nil` value in the model is serialized as YAML `null`.
5897
-
5898
- .3+| JSON (supports all 3 types)
5899
- | `empty` | `empty` | An empty array (`[]`) in the model is serialized as `empty` (an empty JSON array).
5900
- | `omitted` | `omitted` | If the value is omitted in the model, the key is omitted from the JSON output.
5901
- | `nil` | `nil` | A `nil` value in the model is serialized as JSON `null`.
5902
-
5903
- .3+| TOML (does not support `nil`)
5904
- | `empty` | `empty` | An empty array (`[]`) in the model is serialized as `empty` (an empty TOML array).
5905
- | `omitted` | `omitted` | If the value is omitted in the model, the key is omitted from the TOML output.
5906
- | `nil` | `omitted` | In TOML, `nil` is represented as `omitted` due to TOML limitations.
5907
-
5908
- |===
5909
-
5910
-
5911
8425
  Here's an example of how you can use the `value_map` with a collection attribute.
5912
8426
 
5913
8427
  .Using `value_map` with a collection attribute
@@ -5925,12 +8439,19 @@ class ExampleClass < Lutaml::Model::Serializable
5925
8439
  }
5926
8440
  end
5927
8441
 
5928
- json | yaml | toml | key_value do
8442
+ hsh | json | yaml | key_value do
5929
8443
  map 'status', to: :status, value_map: {
5930
8444
  from: { empty: :nil, omitted: :omitted, nil: :nil },
5931
8445
  to: { empty: :nil, omitted: :omitted, nil: :nil }
5932
8446
  }
5933
8447
  end
8448
+
8449
+ toml do
8450
+ map 'status', to: :status, value_map: {
8451
+ from: { empty: :nil, omitted: :omitted },
8452
+ to: { empty: :nil, omitted: :omitted, nil: :omitted }
8453
+ }
8454
+ end
5934
8455
  end
5935
8456
 
5936
8457
  yaml = <<~YAML
@@ -5943,9 +8464,9 @@ y = ExampleClass.from_yaml(yaml)
5943
8464
  ----
5944
8465
  ====
5945
8466
 
5946
- TODO: Need to improve this example.
8467
+ // TODO: Need to improve this example.
5947
8468
 
5948
- ==== Specific overrides of value map
8469
+ ==== Specific overrides of value map (`render_*` and `treat_*`)
5949
8470
 
5950
8471
  ===== General
5951
8472
 
@@ -5982,6 +8503,52 @@ missing value types into the model.
5982
8503
  `{format-value}`::: specifies the missing value type in the serialization format.
5983
8504
  `{model-value}`::: specifies the missing value type in the LutaML Model.
5984
8505
 
8506
+ In effect, the default `value_map` is overriden by the `:render_*` and `:treat_*`
8507
+ directives.
8508
+
8509
+ [example]
8510
+ ====
8511
+ Given the default mapping for an XML element, the `:render_*` and `:treat_*`
8512
+ options can be used to selectively override behavior.
8513
+
8514
+ [source,ruby]
8515
+ ----
8516
+ xml do
8517
+ map_element 'key', to: :attr, value_map: {
8518
+ from: { empty: :nil, omitted: :omitted, nil: :nil },
8519
+ to: { empty: :empty, omitted: :omitted, nil: :nil }
8520
+ }
8521
+ end
8522
+ ----
8523
+
8524
+ By changing to this:
8525
+
8526
+ [source,ruby]
8527
+ ----
8528
+ xml do
8529
+ map_element 'key', to: :attr,
8530
+ render_nil: :as_empty, <1>
8531
+ treat_omitted: :as_nil <2>
8532
+ end
8533
+ ----
8534
+ <1> This overrides the `to:` direction `nil: :nil` mapping.
8535
+ <2> This overrides the `from:` direction `omitted: :omitted` mapping
8536
+
8537
+ The resulting value map would be:
8538
+
8539
+ [source,ruby]
8540
+ ----
8541
+ xml do
8542
+ map_element 'status', to: :status, value_map: {
8543
+ from: { empty: :nil, omitted: :omitted, nil: :empty }, <1>
8544
+ to: { empty: :nil, omitted: :nil, nil: :nil } <2>
8545
+ }
8546
+ end
8547
+ ----
8548
+ <1> See that `nil: :nil` is now `nil: :empty`.
8549
+ <2> See that `omitted: :omitted` is now `omitted: :nil`
8550
+ ====
8551
+
5985
8552
 
5986
8553
  ==== `render_nil`
5987
8554
 
@@ -6014,7 +8581,7 @@ end
6014
8581
 
6015
8582
  [source,ruby]
6016
8583
  ----
6017
- json | yaml | toml do
8584
+ hsh | json | yaml | toml do
6018
8585
  map 'key_value_model_attribute_name', to: :name_of_attribute, render_nil: {option}
6019
8586
  end
6020
8587
  ----
@@ -6153,7 +8720,7 @@ class SomeModel < Lutaml::Model::Serializable
6153
8720
  map_element 'collection', to: :coll, render_nil: :as_nil
6154
8721
  end
6155
8722
 
6156
- json | yaml do
8723
+ hsh | json | yaml do
6157
8724
  map 'collection', to: :coll, render_nil: :as_nil
6158
8725
  end
6159
8726
  end
@@ -6238,7 +8805,7 @@ end
6238
8805
 
6239
8806
  [source,ruby]
6240
8807
  ----
6241
- json | yaml | toml do
8808
+ hsh | json | yaml | toml do
6242
8809
  map 'key_value_model_attribute_name', to: :name_of_attribute, render_empty: {option}
6243
8810
  end
6244
8811
  ----
@@ -6297,7 +8864,7 @@ class SomeModel < Lutaml::Model::Serializable
6297
8864
  map_element 'collection', to: :coll, render_empty: :as_nil
6298
8865
  end
6299
8866
 
6300
- json | yaml do
8867
+ hsh | json | yaml do
6301
8868
  map 'collection', to: :coll, render_empty: :as_nil
6302
8869
  end
6303
8870
  end
@@ -6356,9 +8923,91 @@ puts SomeModel.new.to_yaml
6356
8923
 
6357
8924
 
6358
8925
 
6359
- == Importing data models
8926
+ == Schema generation and import
6360
8927
 
6361
- === General
8928
+ === Schema generation
8929
+
8930
+ Lutaml::Model provides functionality to generate schema definitions from LutaML models. This allows you to create schemas that can be used for validation or documentation purposes.
8931
+
8932
+ Currently, the following schema formats are supported:
8933
+
8934
+ * JSON Schema (https://json-schema.org/understanding-json-schema/[JSON Schema])
8935
+ * YAML Schema (https://yaml.org/spec/1.2/spec.html[YAML])
8936
+
8937
+ ==== JSON Schema generation
8938
+
8939
+ The `Lutaml::Model::Schema.to_json` method generates a JSON Schema from a LutaML model class. The generated schema includes:
8940
+
8941
+ * Properties based on model attributes
8942
+ * Validation constraints (patterns, enumerations, etc.)
8943
+ * Support for polymorphic types
8944
+ * Support for inheritance
8945
+ * Support for choice attributes
8946
+ * Collection constraints
8947
+
8948
+ Example:
8949
+
8950
+ [source,ruby]
8951
+ ----
8952
+ class Glaze < Lutaml::Model::Serializable
8953
+ attribute :color, :string
8954
+ attribute :finish, :string
8955
+ end
8956
+
8957
+ class Vase < Lutaml::Model::Serializable
8958
+ attribute :height, :float
8959
+ attribute :diameter, :float
8960
+ attribute :glaze, Glaze
8961
+ attribute :materials, :string, collection: true
8962
+ end
8963
+
8964
+ # Generate JSON schema
8965
+ schema = Lutaml::Model::Schema.to_json(
8966
+ Vase,
8967
+ id: "https://example.com/vase.schema.json",
8968
+ description: "A vase schema",
8969
+ pretty: true
8970
+ )
8971
+
8972
+ # Write to file
8973
+ File.write("vase.schema.json", schema)
8974
+ ----
8975
+
8976
+ The generated schema will include definitions for all nested models and their attributes.
8977
+
8978
+ ==== YAML Schema generation
8979
+
8980
+ The `Lutaml::Model::Schema.to_yaml` method generates a YAML Schema from a LutaML model class. The generated schema includes the same features as the JSON Schema generation.
8981
+
8982
+ Example:
8983
+
8984
+ [source,ruby]
8985
+ ----
8986
+ class Glaze < Lutaml::Model::Serializable
8987
+ attribute :color, :string
8988
+ attribute :finish, :string
8989
+ end
8990
+
8991
+ class Vase < Lutaml::Model::Serializable
8992
+ attribute :height, :float
8993
+ attribute :diameter, :float
8994
+ attribute :glaze, Glaze
8995
+ attribute :materials, :string, collection: true
8996
+ end
8997
+
8998
+ # Generate YAML schema
8999
+ schema = Lutaml::Model::Schema.to_yaml(
9000
+ Vase,
9001
+ id: "http://example.com/schemas/vase",
9002
+ description: "A vase schema",
9003
+ pretty: true
9004
+ )
9005
+
9006
+ # Write to file
9007
+ File.write("vase.schema.yaml", schema)
9008
+ ----
9009
+
9010
+ === Importing data models
6362
9011
 
6363
9012
  Lutaml::Model provides a way to import data models defined from various formats
6364
9013
  into the LutaML data modeling system.
@@ -6883,57 +9532,129 @@ puts output
6883
9532
  ----
6884
9533
  ====
6885
9534
 
6886
- == Adapters
9535
+
9536
+ == Serialization adapters
6887
9537
 
6888
9538
  === General
6889
9539
 
6890
- Lutaml::Model uses an adapter pattern to support multiple libraries for each
6891
- serialization format.
9540
+ The LutaML component that serializes a model into a serialization format is
9541
+ called an adapter. A serialization format may be supported by multiple adapters.
9542
+
9543
+ An adapter typically:
6892
9544
 
6893
- Lutaml::Model supports the following serialization formats:
9545
+ * supports a specific serialization format
9546
+ * provides a set of methods to serialize and deserialize models and collections of models
9547
+
9548
+ LutaML, out of the box, supports the following serialization formats:
6894
9549
 
6895
9550
  * XML (https://www.w3.org/TR/xmlschema-1/[W3C XML Schema (Second Edition)], XML 1.0)
6896
9551
  * YAML (https://yaml.org/[YAML version 1.2])
6897
9552
  * JSON (https://www.ecma-international.org/publications-and-standards/standards/ecma-404/[ECMA-404 The JSON Data Interchange Standard], unofficial link: https://www.json.org[JSON])
6898
9553
  * TOML (https://toml.io/en[TOML version 1.0])
6899
9554
 
6900
- You will need to specify the configuration for the adapter you want to use. The
6901
- easiest way is to copy and paste the following configuration into your code.
9555
+ The adapter interface is also used to support certain transformation of models
9556
+ into an "end format", which is not a serialization format. For example, the
9557
+ `Lutaml::Model::HashAdapter` is used to convert a model into a hash format
9558
+ that is not a serialization format.
9559
+
9560
+ Users can extend LutaML by creating custom adapters for other serialization
9561
+ formats or for other data formats. The
9562
+ link:docs/custom_adapters.adoc[Custom Adapters Guide] describes this process in
9563
+ detail.
6902
9564
 
6903
- The configuration is as follows:
9565
+ For certain serialization formats, LutaML provides multiple adapters to support
9566
+ different serialization libraries. Please refer to their specific sections for
9567
+ more information.
9568
+
9569
+
9570
+ === Configuration
9571
+
9572
+ ==== General
9573
+
9574
+ It is necessary to configure the adapter to be used for serialization and
9575
+ deserialization for a set of formats that the LutaML models will be transformed
9576
+ into.
9577
+
9578
+ There are two cases where you need to define such configuration:
9579
+
9580
+ * End-user usage of the LutaML models. This is the case where you are
9581
+ using LutaML models in your application and want to serialize them into a
9582
+ specific format. If you are a gem developer that relies on lutaml-model,
9583
+ this case does not apply to you, because the end-user of your gem should
9584
+ determine the adapter configuration.
9585
+
9586
+ * Testing purposes, e.g. RSpec. In order to run tests that involve verifying
9587
+ correctness of serialization, it is necessary to define adapter configuration.
9588
+
9589
+ There are two ways to specify a configuration:
9590
+
9591
+ * by providing a predefined symbol (preferred)
9592
+ * by providing the actual adapter classes
9593
+
9594
+ There is a default configuration for adapters for commonly used formats:
9595
+
9596
+ * YAML: `yaml_adapter_type` is set to `:standard_yaml`
9597
+ * JSON: `json_adapter_type` is set to `:standard_json`
9598
+ * Hash: `hash_adapter_type` is set to `:standard_hash`
9599
+ * XML: not defined
9600
+ * TOML: not defined
9601
+
9602
+
9603
+ ==== Configure adapters through symbol choices
9604
+
9605
+ The end-user or a gem developer can copy and paste the following configuration
9606
+ into an early loading file in their application or gem.
9607
+
9608
+ This configuration is preferred over the class choices because it is more
9609
+ concise and does not require any `require` code specific to the internals
9610
+ of the LutaML runtime implementation.
9611
+
9612
+ Syntax:
6904
9613
 
6905
9614
  [source,ruby]
6906
9615
  ----
6907
9616
  require 'lutaml/model'
6908
- require 'lutaml/model/xml_adapter/nokogiri_adapter'
6909
- require 'lutaml/model/json_adapter/standard_json_adapter'
6910
- require 'lutaml/model/toml_adapter/toml_rb_adapter'
6911
- require 'lutaml/model/yaml_adapter/standard_yaml_adapter'
6912
9617
 
6913
9618
  Lutaml::Model::Config.configure do |config|
6914
- config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
6915
- config.yaml_adapter = Lutaml::Model::YamlAdapter::StandardYamlAdapter
6916
- config.json_adapter = Lutaml::Model::JsonAdapter::StandardJsonAdapter
6917
- config.toml_adapter = Lutaml::Model::TomlAdapter::TomlRbAdapter
9619
+ config.xml_adapter_type = :nokogiri # can be one of [:nokogiri, :ox, :oga]
9620
+ config.hash_adapter_type = :standard_hash
9621
+ config.yaml_adapter_type = :standard_yaml
9622
+ config.json_adapter_type = :standard_json # can be one of [:standard_json, :multi_json]
9623
+ config.toml_adapter_type = :toml_rb # can be one of [:toml_rb, :tomlib]
6918
9624
  end
6919
9625
  ----
6920
9626
 
6921
- You can also provide the adapter type by using symbols like
6922
9627
 
9628
+ ==== Configure adapters through class choices
9629
+
9630
+ The end-uesr or a gem developer can copy and paste the following configuration
9631
+ into an early loading file in their application or gem.
9632
+
9633
+ Only the serialization formats used will require a configuration.
9634
+
9635
+ Syntax:
9636
+
9637
+ [example]
9638
+ ====
6923
9639
  [source,ruby]
6924
9640
  ----
6925
9641
  require 'lutaml/model'
9642
+ require 'lutaml/model/xml/nokogiri_adapter'
9643
+ require 'lutaml/model/hash_adapter/standard_adapter'
9644
+ require 'lutaml/model/json/standard_adapter'
9645
+ require 'lutaml/model/yaml/standard_adapter'
9646
+ require 'lutaml/model/toml/toml_rb_adapter'
6926
9647
 
6927
9648
  Lutaml::Model::Config.configure do |config|
6928
- config.xml_adapter_type = :nokogiri # can be one of [:nokogiri, :ox, :oga]
6929
- config.yaml_adapter_type = :standard_yaml
6930
- config.json_adapter_type = :standard_json # can be one of [:standard_json, :multi_json]
6931
- config.toml_adapter_type = :toml_rb # can be one of [:toml_rb, :tomlib]
9649
+ config.xml_adapter = Lutaml::Model::Xml::NokogiriAdapter
9650
+ config.hash_adapter = Lutaml::Model::HashAdapter::StandardAdapter
9651
+ config.yaml_adapter = Lutaml::Model::Yaml::StandardAdapter
9652
+ config.json_adapter = Lutaml::Model::Json::StandardAdapter
9653
+ config.toml_adapter = Lutaml::Model::Toml::TomlRbAdapter
6932
9654
  end
6933
9655
  ----
9656
+ ====
6934
9657
 
6935
- NOTE: By default `yaml_adapter_type` and `json_adapter_type` are set to
6936
- `:standard_yaml` and `:standard_json` respectively.
6937
9658
 
6938
9659
 
6939
9660
  === XML
@@ -6960,36 +9681,17 @@ Requires native extensions (i.e. compiled C code).
6960
9681
  Requires the `ox` gem.
6961
9682
 
6962
9683
 
6963
- .Using the Nokogiri XML adapter
6964
- [source,ruby]
6965
- ----
6966
- require 'lutaml/model'
6967
-
6968
- Lutaml::Model::Config.configure do |config|
6969
- require 'lutaml/model/xml_adapter/nokogiri_adapter'
6970
- config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
6971
- end
6972
- ----
6973
-
6974
- .Using the Oga XML adapter
6975
- [source,ruby]
6976
- ----
6977
- require 'lutaml/model'
6978
-
6979
- Lutaml::Model::Config.configure do |config|
6980
- require 'lutaml/model/xml_adapter/oga_adapter'
6981
- config.xml_adapter = Lutaml::Model::XmlAdapter::OgaAdapter
6982
- end
6983
- ----
6984
-
6985
- .Using the Ox XML adapter
9684
+ .Using an XML adapter
6986
9685
  [source,ruby]
6987
9686
  ----
6988
9687
  require 'lutaml/model'
6989
9688
 
6990
9689
  Lutaml::Model::Config.configure do |config|
6991
- require 'lutaml/model/xml_adapter/ox_adapter'
6992
- config.xml_adapter = Lutaml::Model::XmlAdapter::OxAdapter
9690
+ config.xml_adapter = :nokogiri
9691
+ # or
9692
+ config.xml_adapter = :oga
9693
+ # or
9694
+ config.xml_adapter = :ox
6993
9695
  end
6994
9696
  ----
6995
9697
 
@@ -7003,19 +9705,17 @@ YAML::
7003
9705
  The Psych YAML parser and emitter for Ruby.
7004
9706
  Included in the Ruby standard library.
7005
9707
 
7006
- .Using the YAML adapter
9708
+ .Using a YAML adapter
7007
9709
  [source,ruby]
7008
9710
  ----
7009
9711
  require 'lutaml/model'
7010
9712
 
7011
9713
  Lutaml::Model::Config.configure do |config|
7012
- require 'lutaml/model/yaml_adapter/standard_yaml_adapter'
7013
- config.yaml_adapter = Lutaml::Model::YamlAdapter::StandardYamlAdapter
9714
+ config.yaml_adapter = :standard_yaml
7014
9715
  end
7015
9716
  ----
7016
9717
 
7017
9718
 
7018
-
7019
9719
  === JSON
7020
9720
 
7021
9721
  Lutaml::Model supports the following JSON adapters:
@@ -7030,27 +9730,18 @@ MultiJson::
7030
9730
  A gem that provides a common interface to multiple JSON libraries.
7031
9731
  Requires the `multi_json` gem.
7032
9732
 
7033
- .Using the JSON adapter
9733
+ .Using a JSON adapter
7034
9734
  [source,ruby]
7035
9735
  ----
7036
9736
  require 'lutaml/model'
7037
9737
 
7038
9738
  Lutaml::Model::Config.configure do |config|
7039
- require 'lutaml/model/json_adapter/standard_json_adapter'
7040
- config.json_adapter = Lutaml::Model::JsonAdapter::StandardJsonAdapter
9739
+ config.json_adapter = :standard_json
9740
+ # or
9741
+ config.json_adapter = :multi_json
7041
9742
  end
7042
9743
  ----
7043
9744
 
7044
- .Using the MultiJson adapter
7045
- [source,ruby]
7046
- ----
7047
- require 'lutaml/model'
7048
-
7049
- Lutaml::Model::Config.configure do |config|
7050
- require 'lutaml/model/json_adapter/multi_json_adapter'
7051
- config.json_adapter = Lutaml::Model::JsonAdapter::MultiJsonAdapter
7052
- end
7053
- ----
7054
9745
 
7055
9746
  === TOML
7056
9747
 
@@ -7069,28 +9760,82 @@ additional features.
7069
9760
  Requires the `tomlib` gem.
7070
9761
 
7071
9762
 
7072
- .Using the Toml-rb adapter
9763
+ .Using a TOML adapter
7073
9764
  [source,ruby]
7074
9765
  ----
7075
9766
  require 'lutaml/model'
7076
9767
 
7077
9768
  Lutaml::Model::Config.configure do |config|
7078
- require 'lutaml/model/toml_adapter/toml_rb_adapter'
7079
- config.toml_adapter = Lutaml::Model::TomlAdapter::TomlRbAdapter
9769
+ config.toml_adapter = :toml_rb
9770
+ # or
9771
+ config.toml_adapter = :tomlib
7080
9772
  end
7081
9773
  ----
7082
9774
 
7083
- .Using the Tomlib adapter
7084
- [source,ruby]
7085
- ----
7086
- require 'lutaml/model'
7087
9775
 
7088
- Lutaml::Model::Config.configure do |config|
7089
- config.toml_adapter = Lutaml::Model::TomlAdapter::TomlibAdapter
7090
- require 'lutaml/model/toml_adapter/tomlib_adapter'
7091
- end
7092
- ----
9776
+ [[custom-adapters]]
9777
+ == Custom serialization adapters
9778
+
9779
+ Lutaml::Model provides a flexible system for creating custom adapters to handle
9780
+ different data formats.
9781
+
9782
+ Please refer to link:docs/custom_adapters.adoc[Custom adapters] for details
9783
+ and examples.
9784
+
9785
+
9786
+ [[schema-generation]]
9787
+ == Schema generation
9788
+
9789
+ Lutaml::Model provides functionality to generate schema definitions from LutaML models. This allows you to create schemas that can be used for validation or documentation purposes.
9790
+
9791
+ Currently, the following schema formats are supported:
9792
+
9793
+ * JSON Schema (https://json-schema.org/understanding-json-schema/[JSON Schema])
9794
+ * YAML Schema (https://yaml.org/spec/1.2/spec.html[YAML])
9795
+ * XSD (https://w3.org/TR/xmlschema-1/[XML Schema Definition Language])
9796
+ * RELAX NG (https://relaxng.org/[RELAX NG])
9797
+
9798
+ The schema generation supports advanced features such as:
7093
9799
 
9800
+ * Validation constraints (patterns, enumerations, ranges)
9801
+ * Choice attributes with min/max constraints
9802
+ * Polymorphic types with oneOf validation
9803
+ * Collection constraints with minItems/maxItems
9804
+
9805
+ Please refer to link:docs/schema_generation.adoc[Schema Generation] for details
9806
+ and examples.
9807
+
9808
+
9809
+ [[schema-import]]
9810
+ == Schema import
9811
+
9812
+ Lutaml::Model provides functionality to import schema definitions into LutaML
9813
+ models. This allows you to create models from existing schema definitions.
9814
+
9815
+ Currently, the following schema formats are supported:
9816
+
9817
+ * XSD (https://w3.org/TR/xmlschema-1/[XML Schema Definition Language])
9818
+ * JSON/YAML Schema
9819
+
9820
+ Please refer to link:docs/schema_import.adoc[Schema Import] for details
9821
+ and examples.
9822
+
9823
+
9824
+ [[custom-registers]]
9825
+ === Custom Registers
9826
+
9827
+ A *LutaML::Model* *Register* allows for dynamic modification and reconfiguration of model hierarchies without altering the original model definitions.
9828
+ For more information, refer to the link:docs/custom_registers.adoc[Custom Registers Guide].
9829
+
9830
+ NOTE: Before using the `Lutaml::Model::Register` instance, make sure to register it in `Lutaml::Model::GlobalRegister`.
9831
+
9832
+ NOTE: By default, a `default_register` with the id `:default` is created and registered in the GlobalRegister. This default register is also set in `Lutaml::Model::Config.default_register` as the default value.
9833
+
9834
+ The default register can be set at the configuration level using the following syntax:
9835
+
9836
+ ```ruby
9837
+ Lutaml::Model::Config.default_register = :default # the register id goes here.
9838
+ ```
7094
9839
 
7095
9840
  == Comparison with Shale
7096
9841
 
@@ -7205,6 +9950,11 @@ data models.
7205
9950
  | No.
7206
9951
  | Lutaml::Model supports attribute extraction from key-value data models.
7207
9952
 
9953
+ | Register
9954
+ | Yes. Supports three types of registers(<<custom-register, read more details>>) with different types of functionalities.
9955
+ | Supports `register` functionality for *Shale::Type* classes only.
9956
+ | `Lutaml::Model` registers both `Registrable` classes and `Lutaml::Model::Type` classes, offering a more comprehensive registration system.
9957
+
7208
9958
  |===
7209
9959
 
7210
9960