lutaml-model 0.7.3 → 0.7.6

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 (185) 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_todo.yml +25 -52
  10. data/README.adoc +2294 -192
  11. data/docs/custom_registers.adoc +228 -0
  12. data/docs/schema_generation.adoc +898 -0
  13. data/docs/schema_import.adoc +364 -0
  14. data/flake.lock +114 -0
  15. data/flake.nix +103 -0
  16. data/lib/lutaml/model/attribute.rb +230 -94
  17. data/lib/lutaml/model/choice.rb +30 -0
  18. data/lib/lutaml/model/collection.rb +195 -0
  19. data/lib/lutaml/model/comparable_model.rb +3 -3
  20. data/lib/lutaml/model/config.rb +26 -3
  21. data/lib/lutaml/model/constants.rb +2 -0
  22. data/lib/lutaml/model/error/element_count_out_of_range_error.rb +29 -0
  23. data/lib/lutaml/model/error/invalid_attribute_name_error.rb +15 -0
  24. data/lib/lutaml/model/error/invalid_attribute_options_error.rb +16 -0
  25. data/lib/lutaml/model/error/invalid_choice_range_error.rb +3 -5
  26. data/lib/lutaml/model/error/register/not_registrable_class_error.rb +11 -0
  27. data/lib/lutaml/model/error/type/invalid_value_error.rb +5 -3
  28. data/lib/lutaml/model/error/type/max_bound_error.rb +20 -0
  29. data/lib/lutaml/model/error/type/max_length_error.rb +20 -0
  30. data/lib/lutaml/model/error/type/min_bound_error.rb +20 -0
  31. data/lib/lutaml/model/error/type/min_length_error.rb +20 -0
  32. data/lib/lutaml/model/error/type/pattern_not_matched_error.rb +18 -0
  33. data/lib/lutaml/model/error/validation_failed_error.rb +9 -0
  34. data/lib/lutaml/model/error.rb +10 -0
  35. data/lib/lutaml/model/errors.rb +36 -0
  36. data/lib/lutaml/model/format_registry.rb +5 -2
  37. data/lib/lutaml/model/global_register.rb +41 -0
  38. data/lib/lutaml/model/{hash.rb → hash_adapter.rb} +5 -5
  39. data/lib/lutaml/model/jsonl/document.rb +14 -0
  40. data/lib/lutaml/model/jsonl/mapping.rb +19 -0
  41. data/lib/lutaml/model/jsonl/mapping_rule.rb +9 -0
  42. data/lib/lutaml/model/jsonl/standard_adapter.rb +33 -0
  43. data/lib/lutaml/model/jsonl/transform.rb +19 -0
  44. data/lib/lutaml/model/jsonl.rb +21 -0
  45. data/lib/lutaml/model/key_value_document.rb +3 -2
  46. data/lib/lutaml/model/mapping/key_value_mapping.rb +64 -4
  47. data/lib/lutaml/model/mapping/key_value_mapping_rule.rb +4 -0
  48. data/lib/lutaml/model/mapping/mapping_rule.rb +8 -3
  49. data/lib/lutaml/model/register.rb +105 -0
  50. data/lib/lutaml/model/registrable.rb +6 -0
  51. data/lib/lutaml/model/schema/base_schema.rb +64 -0
  52. data/lib/lutaml/model/schema/decorators/attribute.rb +114 -0
  53. data/lib/lutaml/model/schema/decorators/choices.rb +31 -0
  54. data/lib/lutaml/model/schema/decorators/class_definition.rb +85 -0
  55. data/lib/lutaml/model/schema/decorators/definition_collection.rb +97 -0
  56. data/lib/lutaml/model/schema/generator/definition.rb +53 -0
  57. data/lib/lutaml/model/schema/generator/definitions_collection.rb +81 -0
  58. data/lib/lutaml/model/schema/generator/properties_collection.rb +63 -0
  59. data/lib/lutaml/model/schema/generator/property.rb +110 -0
  60. data/lib/lutaml/model/schema/generator/ref.rb +24 -0
  61. data/lib/lutaml/model/schema/helpers/template_helper.rb +49 -0
  62. data/lib/lutaml/model/schema/json_schema.rb +42 -49
  63. data/lib/lutaml/model/schema/relaxng_schema.rb +14 -10
  64. data/lib/lutaml/model/schema/renderer.rb +36 -0
  65. data/lib/lutaml/model/schema/shared_methods.rb +24 -0
  66. data/lib/lutaml/model/schema/templates/model.erb +9 -0
  67. data/lib/lutaml/model/schema/xml_compiler/attribute.rb +85 -0
  68. data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +45 -0
  69. data/lib/lutaml/model/schema/xml_compiler/choice.rb +65 -0
  70. data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +27 -0
  71. data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +34 -0
  72. data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +136 -0
  73. data/lib/lutaml/model/schema/xml_compiler/element.rb +104 -0
  74. data/lib/lutaml/model/schema/xml_compiler/group.rb +97 -0
  75. data/lib/lutaml/model/schema/xml_compiler/restriction.rb +101 -0
  76. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +50 -0
  77. data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +36 -0
  78. data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +189 -0
  79. data/lib/lutaml/model/schema/xml_compiler.rb +231 -587
  80. data/lib/lutaml/model/schema/xsd_schema.rb +12 -8
  81. data/lib/lutaml/model/schema/yaml_schema.rb +41 -35
  82. data/lib/lutaml/model/schema.rb +1 -0
  83. data/lib/lutaml/model/sequence.rb +60 -30
  84. data/lib/lutaml/model/serialize.rb +177 -54
  85. data/lib/lutaml/model/services/base.rb +11 -0
  86. data/lib/lutaml/model/services/logger.rb +2 -2
  87. data/lib/lutaml/model/services/rule_value_extractor.rb +92 -0
  88. data/lib/lutaml/model/services/type/validator/number.rb +25 -0
  89. data/lib/lutaml/model/services/type/validator/string.rb +52 -0
  90. data/lib/lutaml/model/services/type/validator.rb +43 -0
  91. data/lib/lutaml/model/services/validator.rb +145 -0
  92. data/lib/lutaml/model/services.rb +3 -0
  93. data/lib/lutaml/model/transform/key_value_transform.rb +68 -62
  94. data/lib/lutaml/model/transform/xml_transform.rb +46 -57
  95. data/lib/lutaml/model/transform.rb +23 -8
  96. data/lib/lutaml/model/type/boolean.rb +1 -1
  97. data/lib/lutaml/model/type/date.rb +1 -1
  98. data/lib/lutaml/model/type/date_time.rb +1 -1
  99. data/lib/lutaml/model/type/decimal.rb +11 -9
  100. data/lib/lutaml/model/type/float.rb +2 -1
  101. data/lib/lutaml/model/type/integer.rb +24 -21
  102. data/lib/lutaml/model/type/string.rb +4 -2
  103. data/lib/lutaml/model/type/time.rb +1 -1
  104. data/lib/lutaml/model/type/time_without_date.rb +1 -1
  105. data/lib/lutaml/model/type/value.rb +5 -1
  106. data/lib/lutaml/model/type.rb +5 -2
  107. data/lib/lutaml/model/utils.rb +30 -8
  108. data/lib/lutaml/model/validation.rb +6 -4
  109. data/lib/lutaml/model/version.rb +1 -1
  110. data/lib/lutaml/model/xml/document.rb +37 -19
  111. data/lib/lutaml/model/xml/mapping.rb +74 -13
  112. data/lib/lutaml/model/xml/mapping_rule.rb +10 -2
  113. data/lib/lutaml/model/xml/nokogiri_adapter.rb +5 -3
  114. data/lib/lutaml/model/xml/oga/element.rb +4 -1
  115. data/lib/lutaml/model/xml/oga_adapter.rb +4 -3
  116. data/lib/lutaml/model/xml/ox_adapter.rb +20 -6
  117. data/lib/lutaml/model/xml/xml_element.rb +3 -28
  118. data/lib/lutaml/model/xml_adapter/element.rb +1 -1
  119. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +1 -1
  120. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +1 -1
  121. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +1 -1
  122. data/lib/lutaml/model/yamls/document.rb +14 -0
  123. data/lib/lutaml/model/yamls/mapping.rb +19 -0
  124. data/lib/lutaml/model/yamls/mapping_rule.rb +9 -0
  125. data/lib/lutaml/model/yamls/standard_adapter.rb +34 -0
  126. data/lib/lutaml/model/yamls/transform.rb +19 -0
  127. data/lib/lutaml/model/yamls.rb +21 -0
  128. data/lib/lutaml/model.rb +7 -31
  129. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +4 -5
  130. data/spec/fixtures/xml/advanced_test_schema.xsd +134 -0
  131. data/spec/fixtures/xml/examples/nested_categories.xml +55 -0
  132. data/spec/fixtures/xml/examples/valid_catalog.xml +43 -0
  133. data/spec/fixtures/xml/product_catalog.xsd +151 -0
  134. data/spec/fixtures/xml/specifications_schema.xsd +38 -0
  135. data/spec/lutaml/model/attribute_collection_spec.rb +101 -0
  136. data/spec/lutaml/model/attribute_spec.rb +41 -44
  137. data/spec/lutaml/model/choice_spec.rb +44 -0
  138. data/spec/lutaml/model/custom_collection_spec.rb +830 -0
  139. data/spec/lutaml/model/global_register_spec.rb +108 -0
  140. data/spec/lutaml/model/group_spec.rb +9 -3
  141. data/spec/lutaml/model/jsonl/standard_adapter_spec.rb +91 -0
  142. data/spec/lutaml/model/jsonl_spec.rb +229 -0
  143. data/spec/lutaml/model/multiple_mapping_spec.rb +1 -1
  144. data/spec/lutaml/model/register/key_value_spec.rb +275 -0
  145. data/spec/lutaml/model/register/xml_spec.rb +185 -0
  146. data/spec/lutaml/model/register_spec.rb +147 -0
  147. data/spec/lutaml/model/rule_value_extractor_spec.rb +162 -0
  148. data/spec/lutaml/model/schema/generator/definitions_collection_spec.rb +120 -0
  149. data/spec/lutaml/model/schema/json_schema_spec.rb +412 -51
  150. data/spec/lutaml/model/schema/json_schema_to_models_spec.rb +383 -0
  151. data/spec/lutaml/model/schema/xml_compiler/attribute_group_spec.rb +65 -0
  152. data/spec/lutaml/model/schema/xml_compiler/attribute_spec.rb +63 -0
  153. data/spec/lutaml/model/schema/xml_compiler/choice_spec.rb +71 -0
  154. data/spec/lutaml/model/schema/xml_compiler/complex_content_restriction_spec.rb +55 -0
  155. data/spec/lutaml/model/schema/xml_compiler/complex_content_spec.rb +37 -0
  156. data/spec/lutaml/model/schema/xml_compiler/complex_type_spec.rb +173 -0
  157. data/spec/lutaml/model/schema/xml_compiler/element_spec.rb +63 -0
  158. data/spec/lutaml/model/schema/xml_compiler/group_spec.rb +86 -0
  159. data/spec/lutaml/model/schema/xml_compiler/restriction_spec.rb +76 -0
  160. data/spec/lutaml/model/schema/xml_compiler/sequence_spec.rb +59 -0
  161. data/spec/lutaml/model/schema/xml_compiler/simple_content_spec.rb +55 -0
  162. data/spec/lutaml/model/schema/xml_compiler/simple_type_spec.rb +181 -0
  163. data/spec/lutaml/model/schema/xml_compiler_spec.rb +503 -1804
  164. data/spec/lutaml/model/schema/yaml_schema_spec.rb +249 -26
  165. data/spec/lutaml/model/sequence_spec.rb +36 -0
  166. data/spec/lutaml/model/serializable_spec.rb +31 -0
  167. data/spec/lutaml/model/type_spec.rb +8 -4
  168. data/spec/lutaml/model/utils_spec.rb +3 -3
  169. data/spec/lutaml/model/xml/derived_attributes_spec.rb +1 -1
  170. data/spec/lutaml/model/xml/root_mappings/nested_child_mappings_spec.rb +164 -0
  171. data/spec/lutaml/model/xml/xml_element_spec.rb +7 -1
  172. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +6 -6
  173. data/spec/lutaml/model/xml_adapter_spec.rb +24 -0
  174. data/spec/lutaml/model/xml_mapping_rule_spec.rb +11 -4
  175. data/spec/lutaml/model/xml_mapping_spec.rb +1 -1
  176. data/spec/lutaml/model/yamls/standard_adapter_spec.rb +183 -0
  177. data/spec/lutaml/model/yamls_spec.rb +294 -0
  178. data/spec/spec_helper.rb +1 -0
  179. metadata +106 -9
  180. data/lib/lutaml/model/schema/templates/simple_type.rb +0 -247
  181. /data/lib/lutaml/model/{hash → hash_adapter}/document.rb +0 -0
  182. /data/lib/lutaml/model/{hash → hash_adapter}/mapping.rb +0 -0
  183. /data/lib/lutaml/model/{hash → hash_adapter}/mapping_rule.rb +0 -0
  184. /data/lib/lutaml/model/{hash → hash_adapter}/standard_adapter.rb +0 -0
  185. /data/lib/lutaml/model/{hash → hash_adapter}/transform.rb +0 -0
data/README.adoc CHANGED
@@ -39,7 +39,11 @@ Lutaml::Model are provided in <<migrate-from-shale>>.
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>>)
42
44
  * Create custom adapters for additional data formats (see <<custom-adapters>>)
45
+ * Dynamically modify model attribute types using registers (see <<custom-registers>>)
46
+
43
47
 
44
48
 
45
49
  == Data modeling in a nutshell
@@ -270,10 +274,26 @@ Or install it yourself as:
270
274
  gem install lutaml-model
271
275
  ----
272
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]]
273
290
  == Model
274
291
 
275
292
  === General
276
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
+
277
297
  There are two ways to define an information model in Lutaml::Model:
278
298
 
279
299
  * Inheriting from the `Lutaml::Model::Serializable` class
@@ -377,7 +397,7 @@ same class and all their attributes are equal.
377
397
  > # true
378
398
  ----
379
399
 
380
-
400
+ [[value-definition]]
381
401
  == Value types
382
402
 
383
403
  === General types
@@ -565,6 +585,7 @@ part lost due to the inability of JSON to handle high-precision date-time.
565
585
  ====
566
586
 
567
587
 
588
+ [[attribute-definition]]
568
589
  == Attributes
569
590
 
570
591
 
@@ -616,6 +637,56 @@ puts s.established
616
637
  ----
617
638
  ====
618
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
+
619
690
  === Polymorphic attributes
620
691
 
621
692
  ==== General
@@ -821,7 +892,7 @@ end
821
892
  <1> The name of the polymorphic attribute.
822
893
  <2> The polymorphic superclass class.
823
894
  <3> Any options for the attribute.
824
- <4> The `polymorphic` option that determines the acceptable polymorphic subclasses.
895
+ <4> The `polymorphic` option that determines the acceptable polymorphic subclasses, or just `true`.
825
896
  <5> The polymorphic subclasses.
826
897
 
827
898
  The `polymorphic` option is an array of polymorphic subclasses that the
@@ -864,6 +935,25 @@ end
864
935
  ----
865
936
  ====
866
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
+
867
957
  * If the attribute (collection or not) is meant to contain instances belonging
868
958
  to more than one polymorphic subclass, then those acceptable polymorphic
869
959
  subclasses should be explicitly specified in the `polymorphic: [...]` option.
@@ -1485,9 +1575,13 @@ puts SomeModel.new.to_yaml
1485
1575
  ----
1486
1576
  ====
1487
1577
 
1578
+
1488
1579
  === Derived attributes
1489
1580
 
1490
- 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.
1491
1585
 
1492
1586
  Syntax:
1493
1587
 
@@ -1573,6 +1667,7 @@ attributes.
1573
1667
  * The last attribute `completeName` is optional.
1574
1668
  ====
1575
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>>.
1576
1671
 
1577
1672
 
1578
1673
  === Importable models for reuse
@@ -1629,6 +1724,7 @@ class ComplexType < Lutaml::Model::Serializable
1629
1724
 
1630
1725
  xml do
1631
1726
  root "GroupOfItems"
1727
+
1632
1728
  map_attribute "tag", to: :tag
1633
1729
  map_content to: :content
1634
1730
  map_element :group, to: :group
@@ -1660,6 +1756,161 @@ end
1660
1756
  ----
1661
1757
  ====
1662
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>>.
1663
1914
 
1664
1915
  [[attribute-value-transform]]
1665
1916
  === Attribute value transform
@@ -1973,22 +2224,1322 @@ For the following XML snippet:
1973
2224
  @name="John Doe",
1974
2225
  @ordered=nil>
1975
2226
  ----
1976
- ====
2227
+ ====
2228
+
2229
+
2230
+ == Collections
2231
+
2232
+ === General
2233
+
2234
+ Collections are used to represent a contained group of multiple instances of models.
2235
+
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.
2239
+
2240
+ Models in a collection may be:
2241
+
2242
+ * constrained to be of a single kind;
2243
+
2244
+ * constrained to be of multiple kinds sharing common characteristics;
2245
+
2246
+ * unbounded of any kind.
2247
+
2248
+ LutaML Model provides the `Lutaml::Model::Collection` class for defining
2249
+ collections of model instances.
2250
+
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}
2264
+ end
2265
+ ----
2266
+
2267
+ Where,
2268
+
2269
+ `attribute`:: The name of the attribute that contains the collection.
2270
+ `ModelType`:: The model type of the collection elements.
2271
+
2272
+
2273
+ ==== Mapping instances: key-value formats only
2274
+
2275
+ The `map_instances` directive is only used in the `key_value` block.
2276
+
2277
+ Syntax:
2278
+
2279
+ [source,ruby]
2280
+ ----
2281
+ class MyCollection < Lutaml::Model::Collection
2282
+ instances {attribute}, ModelType
2283
+
2284
+ key_value do
2285
+ map_instances to: {attribute}
2286
+ end
2287
+ end
2288
+ ----
2289
+
2290
+ Where,
2291
+
2292
+ `attribute`:: The name of the attribute that contains model instances.
2293
+
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.
2297
+
2298
+
2299
+ ==== Mapping instances: XML only
2300
+
2301
+ In the `xml` block, the `map_element`, `map_attribute` directives are used instead.
2302
+
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:
2308
+
2309
+ [source,ruby]
2310
+ ----
2311
+ class MyCollection < Lutaml::Model::Collection
2312
+ instances {attribute}, ModelType
2313
+
2314
+ xml do
2315
+ map_element "element-name", to: {attribute}
2316
+ end
2317
+ end
2318
+ ----
2319
+
2320
+ Where,
2321
+
2322
+ `element-name`:: The name of the XML element of each model instance.
2323
+
2324
+
2325
+ Syntax for an attribute collection:
2326
+
2327
+ [source,ruby]
2328
+ ----
2329
+ class MyCollection < Lutaml::Model::Collection
2330
+ instances {attribute}, ModelType
2331
+ xml do
2332
+ map_attribute "attribute-name", to: {attribute}
2333
+ end
2334
+ end
2335
+ ----
2336
+
2337
+ Where,
2338
+
2339
+ `attribute-name`:: The name of the XML attribute that contains all model instances.
2340
+
2341
+
2342
+ === Collection types
2343
+
2344
+ A LutaML collections is used for a number of scenarios:
2345
+
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
2353
+
2354
+
2355
+ === Root collections (key-value formats only)
2356
+
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.
1977
3508
 
1978
- == Serialization model mappings
3509
+ There are two kinds of serialization models:
1979
3510
 
1980
- === General
3511
+ * Represents a singular model (maps to a Lutaml::Model::Serializable)
3512
+ * Represents a group/collection of models (maps to Lutaml::Model::Collection)
1981
3513
 
1982
- Lutaml::Model allows you to translate a data model into serialization models of
1983
- various serialization formats including XML, Hash, JSON, YAML, and TOML.
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.
1984
3518
 
1985
- Depending on the serialization format, different methods are supported for
1986
- defining serialization and deserialization mappings.
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)
1987
3538
 
1988
- Serialization model mappings are defined under the `xml`, `hsh`, `json`, `yaml`,
1989
- and `toml` blocks.
1990
3539
 
1991
3540
  .Using the `xml`, `hsh`, `json`, `yaml`, `toml` and `key_value` blocks to define serialization mappings
3541
+ [example]
3542
+ ====
1992
3543
  [source,ruby]
1993
3544
  ----
1994
3545
  class Example < Lutaml::Model::Serializable
@@ -2017,6 +3568,20 @@ class Example < Lutaml::Model::Serializable
2017
3568
  end
2018
3569
  end
2019
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
+ ====
2020
3585
 
2021
3586
 
2022
3587
  === XML
@@ -2988,6 +4553,7 @@ HERE
2988
4553
  ----
2989
4554
  ====
2990
4555
 
4556
+ NOTE: For importing model mappings inside a `sequence` block, refer to <<import-model-mappings-inside-sequence, Importing model mappings inside a `sequence`>>.
2991
4557
 
2992
4558
  [[xml-schema-location]]
2993
4559
  ==== Automatic support of `xsi:schemaLocation`
@@ -3453,11 +5019,20 @@ This XML snippet is in UTF-8.
3453
5019
 
3454
5020
  ==== General
3455
5021
 
3456
- Key-value data models like Hash, JSON, YAML, and TOML all share a similar structure
3457
- 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.
3458
5024
 
3459
5025
  `Lutaml::Model` works with these formats in a similar way.
3460
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
+
3461
5036
  ==== Mapping
3462
5037
 
3463
5038
  The `map` method is used to define key-value mappings.
@@ -3466,11 +5041,35 @@ Syntax:
3466
5041
 
3467
5042
  [source,ruby]
3468
5043
  ----
3469
- hsh | json | yaml | toml | key_value do
5044
+ {key_value_type_short} do <1>
3470
5045
  map 'key_value_model_attribute_name', to: :name_of_attribute
3471
5046
  end
3472
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
+ ====
3473
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
+ ====
3474
5073
 
3475
5074
  ==== Unified mapping
3476
5075
 
@@ -3494,19 +5093,11 @@ class CeramicModel < Lutaml::Model::Serializable
3494
5093
  attribute :glaze, :string
3495
5094
  attribute :description, :string
3496
5095
 
3497
- key_value do
3498
- map :color, to: color
5096
+ key_value do # or any other key-value data model
5097
+ map :color, to: :color
3499
5098
  map :glz, to: :glaze
3500
5099
  map :desc, to: :description
3501
5100
  end
3502
-
3503
- # Equivalent to the Hash, JSON, YAML, and TOML mappings.
3504
- #
3505
- # hsh and json and yaml and toml do
3506
- # map :id, to: color
3507
- # map :name, to: :full_name
3508
- # map :status, to: :current_status
3509
- # end
3510
5101
  end
3511
5102
  ----
3512
5103
 
@@ -3537,13 +5128,7 @@ desc: A ceramic with a navy blue color and clear glaze.
3537
5128
 
3538
5129
  ==== Specific format mappings
3539
5130
 
3540
- Specific key value formats can be mapping independently of other formats, including:
3541
-
3542
- * `hsh` for the Hash format
3543
- * `json` for the JSON format
3544
- * `yaml` for the YAML format
3545
- * `toml` for the TOML format
3546
-
5131
+ Specific key value formats can be mapping independently of other formats.
3547
5132
 
3548
5133
  .Using the `map` method to define key-value mappings per format
3549
5134
  [example]
@@ -4477,106 +6062,447 @@ object as the value of a designated attribute.
4477
6062
  ----
4478
6063
  ====
4479
6064
 
4480
- If a specified value path is not found, the corresponding attribute in the model
4481
- will be assigned a `nil` value.
6065
+ If a specified value path is not found, the corresponding attribute in the model
6066
+ will be assigned a `nil` value.
6067
+
6068
+ .Attribute values set to `nil` when the `path_to_value` is not found
6069
+ [example]
6070
+ ====
6071
+ In the following JSON content, the `path_to_value` of `[:extras, :sunroof]` and
6072
+ `[:extras, :drinks_cooler]` at the object `"gearbox"` would be set to `nil`.
6073
+
6074
+ [source,json]
6075
+ ----
6076
+ {
6077
+ "components": {
6078
+ "engine": {
6079
+ "manufacturer": "Ford",
6080
+ "extras": {
6081
+ "sunroof": true,
6082
+ "drinks_cooler": true
6083
+ }
6084
+ },
6085
+ "gearbox": {
6086
+ "manufacturer": "Toyota"
6087
+ }
6088
+ }
6089
+ }
6090
+ ----
6091
+ ====
6092
+
6093
+
6094
+ .Using the `child_mappings` option to extract values from a key-value data model
6095
+ [example]
6096
+ ====
6097
+ The following JSON contains 2 keys in schema named `foo` and `bar`.
6098
+
6099
+ [source,json]
6100
+ ----
6101
+ {
6102
+ "schemas": {
6103
+ "foo": { <1>
6104
+ "path": { <2>
6105
+ "link": "link one",
6106
+ "name": "one"
6107
+ }
6108
+ },
6109
+ "bar": { <1>
6110
+ "path": { <2>
6111
+ "link": "link two",
6112
+ "name": "two"
6113
+ }
6114
+ }
6115
+ }
6116
+ }
6117
+ ----
6118
+ <1> The keys `foo` and `bar` are to be mapped to the `id` attribute.
6119
+ <2> The nested `path.link` and `path.name` keys are used as the `link` and
6120
+ `name` attributes, respectively.
6121
+
6122
+ A model can be defined for this JSON as follows:
6123
+
6124
+ [source,ruby]
6125
+ ----
6126
+ class Schema < Lutaml::Model::Serializable
6127
+ attribute :id, :string
6128
+ attribute :link, :string
6129
+ attribute :name, :string
6130
+ end
6131
+
6132
+ class ChildMappingClass < Lutaml::Model::Serializable
6133
+ attribute :schemas, Schema, collection: true
6134
+
6135
+ json do
6136
+ map "schemas", to: :schemas,
6137
+ child_mappings: {
6138
+ id: :key,
6139
+ link: %i[path link],
6140
+ name: %i[path name],
6141
+ }
6142
+ end
6143
+ end
6144
+ ----
6145
+
6146
+ The output becomes:
6147
+
6148
+ [source,ruby]
6149
+ ----
6150
+ > ChildMappingClass.from_json(json)
6151
+ > #<ChildMappingClass:0x0000000104ac7240
6152
+ @schemas=
6153
+ [#<Schema:0x0000000104ac6e30 @id="foo", @link="link one", @name="one">,
6154
+ #<Schema:0x0000000104ac58f0 @id="bar", @link="link two", @name="two">]>
6155
+ > ChildMappingClass.new(schemas: [Schema.new(id: "foo", link: "link one", name: "one"), Schema.new(id: "bar", link: "link two", name: "two")]).to_json
6156
+ > #{"schemas"=>{"foo"=>{"path"=>{"link"=>"link one", "name"=>"one"}}, {"bar"=>{"path"=>{"link"=>"link two", "name"=>"two"}}}}}
6157
+ ----
6158
+
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
+
4482
6361
 
4483
- .Attribute values set to `nil` when the `path_to_value` is not found
4484
- [example]
4485
- ====
4486
- In the following JSON content, the `path_to_value` of `[:extras, :sunroof]` and
4487
- `[:extras, :drinks_cooler]` at the object `"gearbox"` would be set to `nil`.
6362
+ ==== YAML Stream
4488
6363
 
4489
- [source,json]
4490
- ----
4491
- {
4492
- "components": {
4493
- "engine": {
4494
- "manufacturer": "Ford",
4495
- "extras": {
4496
- "sunroof": true,
4497
- "drinks_cooler": true
4498
- }
4499
- },
4500
- "gearbox": {
4501
- "manufacturer": "Toyota"
4502
- }
4503
- }
4504
- }
4505
- ----
4506
- ====
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.
4507
6368
 
6369
+ It represents a collection of YAML documents encoded one document per stream.
4508
6370
 
4509
- .Using the `child_mappings` option to extract values from a key-value data model
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
4510
6384
  [example]
4511
6385
  ====
4512
- The following JSON contains 2 keys in schema named `foo` and `bar`.
4513
-
4514
- [source,json]
4515
- ----
4516
- {
4517
- "schemas": {
4518
- "foo": { <1>
4519
- "path": { <2>
4520
- "link": "link one",
4521
- "name": "one"
4522
- }
4523
- },
4524
- "bar": { <1>
4525
- "path": { <2>
4526
- "link": "link two",
4527
- "name": "two"
4528
- }
4529
- }
4530
- }
4531
- }
6386
+ [source,ruby]
4532
6387
  ----
4533
- <1> The keys `foo` and `bar` are to be mapped to the `id` attribute.
4534
- <2> The nested `path.link` and `path.name` keys are used as the `link` and
4535
- `name` attributes, respectively.
6388
+ class Person
6389
+ attribute :name, :string
6390
+ attribute :age, :integer
6391
+ attribute :id, :string
6392
+ end
4536
6393
 
4537
- A model can be defined for this JSON as follows:
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)
4538
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
+ ====
4539
6426
  [source,ruby]
4540
6427
  ----
4541
- class Schema < Lutaml::Model::Serializable
4542
- attribute :id, :string
4543
- attribute :link, :string
6428
+ class Person
4544
6429
  attribute :name, :string
6430
+ attribute :age, :integer
6431
+ attribute :id, :string
4545
6432
  end
4546
6433
 
4547
- class ChildMappingClass < Lutaml::Model::Serializable
4548
- attribute :schemas, Schema, collection: true
6434
+ class Directory < Lutaml::Model::Serializeable
6435
+ attribute :persons, Person, collection: true
4549
6436
 
4550
- json do
4551
- map "schemas", to: :schemas,
4552
- child_mappings: {
4553
- id: :key,
4554
- link: %i[path link],
4555
- name: %i[path name],
4556
- }
6437
+ yamls do
6438
+ map_instances to: :persons
4557
6439
  end
4558
6440
  end
4559
- ----
4560
6441
 
4561
- The output becomes:
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
4562
6452
 
4563
- [source,ruby]
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
+ # ]>
4564
6460
  ----
4565
- > ChildMappingClass.from_json(json)
4566
- > #<ChildMappingClass:0x0000000104ac7240
4567
- @schemas=
4568
- [#<Schema:0x0000000104ac6e30 @id="foo", @link="link one", @name="one">,
4569
- #<Schema:0x0000000104ac58f0 @id="bar", @link="link two", @name="two">]>
4570
- > ChildMappingClass.new(schemas: [Schema.new(id: "foo", link: "link one", name: "one"), Schema.new(id: "bar", link: "link two", name: "two")]).to_json
4571
- > #{"schemas"=>{"foo"=>{"path"=>{"link"=>"link one", "name"=>"one"}}, {"bar"=>{"path"=>{"link"=>"link two", "name"=>"two"}}}}}
6461
+ ====
6462
+
6463
+ .Parsing a YAML Stream collection relying on YAML mappings on instance model
6464
+ [example]
6465
+ ====
6466
+ [source,ruby]
4572
6467
  ----
6468
+ class Person
6469
+ attribute :name, :string
6470
+ attribute :age, :integer
6471
+ attribute :id, :string
4573
6472
 
4574
- In this example:
6473
+ yaml do
6474
+ map "full_name", to: :name
6475
+ map "age", to: :age
6476
+ map "id", to: :id
6477
+ end
6478
+ end
4575
6479
 
4576
- * The `key` of each schema (`foo` and `bar`) is mapped to the `id` attribute.
6480
+ class Directory < Lutaml::Model::Collection
6481
+ instances :persons, Person
4577
6482
 
4578
- * The nested `path.link` and `path.name` keys are mapped to the `link` and
4579
- `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
+ ----
4580
6506
  ====
4581
6507
 
4582
6508
  === Format-independent mechanisms
@@ -6997,9 +8923,91 @@ puts SomeModel.new.to_yaml
6997
8923
 
6998
8924
 
6999
8925
 
7000
- == Importing data models
8926
+ == Schema generation and import
7001
8927
 
7002
- === 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
7003
9011
 
7004
9012
  Lutaml::Model provides a way to import data models defined from various formats
7005
9013
  into the LutaML data modeling system.
@@ -7524,60 +9532,129 @@ puts output
7524
9532
  ----
7525
9533
  ====
7526
9534
 
7527
- == Adapters
9535
+
9536
+ == Serialization adapters
7528
9537
 
7529
9538
  === General
7530
9539
 
7531
- Lutaml::Model uses an adapter pattern to support multiple libraries for each
7532
- 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:
7533
9544
 
7534
- 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:
7535
9549
 
7536
9550
  * XML (https://www.w3.org/TR/xmlschema-1/[W3C XML Schema (Second Edition)], XML 1.0)
7537
9551
  * YAML (https://yaml.org/[YAML version 1.2])
7538
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])
7539
9553
  * TOML (https://toml.io/en[TOML version 1.0])
7540
- * You can also create custom adapters for additional data formats. See link:docs/custom_adapters.adoc[Custom Adapters Guide] for detailed information.
7541
9554
 
7542
- You will need to specify the configuration for the adapter you want to use. The
7543
- 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.
9564
+
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
+
7544
9569
 
7545
- The configuration is as follows:
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:
7546
9613
 
7547
9614
  [source,ruby]
7548
9615
  ----
7549
9616
  require 'lutaml/model'
7550
- require 'lutaml/model/xml_adapter/nokogiri_adapter'
7551
- require 'lutaml/model/json_adapter/standard_json_adapter'
7552
- require 'lutaml/model/toml_adapter/toml_rb_adapter'
7553
- require 'lutaml/model/yaml_adapter/standard_yaml_adapter'
7554
9617
 
7555
9618
  Lutaml::Model::Config.configure do |config|
7556
- config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
7557
- config.hash_adapter = Lutaml::Model::YamlAdapter::StandardHashAdapter
7558
- config.yaml_adapter = Lutaml::Model::YamlAdapter::StandardYamlAdapter
7559
- config.json_adapter = Lutaml::Model::JsonAdapter::StandardJsonAdapter
7560
- 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]
7561
9624
  end
7562
9625
  ----
7563
9626
 
7564
- You can also provide the adapter type by using symbols like
7565
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
+ ====
7566
9639
  [source,ruby]
7567
9640
  ----
7568
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'
7569
9647
 
7570
9648
  Lutaml::Model::Config.configure do |config|
7571
- config.xml_adapter_type = :nokogiri # can be one of [:nokogiri, :ox, :oga]
7572
- config.hash_adapter_type = :standard_hash
7573
- config.yaml_adapter_type = :standard_yaml
7574
- config.json_adapter_type = :standard_json # can be one of [:standard_json, :multi_json]
7575
- 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
7576
9654
  end
7577
9655
  ----
9656
+ ====
7578
9657
 
7579
- NOTE: By default `yaml_adapter_type` and `json_adapter_type` are set to
7580
- `:standard_yaml` and `:standard_json` respectively.
7581
9658
 
7582
9659
 
7583
9660
  === XML
@@ -7604,36 +9681,17 @@ Requires native extensions (i.e. compiled C code).
7604
9681
  Requires the `ox` gem.
7605
9682
 
7606
9683
 
7607
- .Using the Nokogiri XML adapter
7608
- [source,ruby]
7609
- ----
7610
- require 'lutaml/model'
7611
-
7612
- Lutaml::Model::Config.configure do |config|
7613
- require 'lutaml/model/xml_adapter/nokogiri_adapter'
7614
- config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
7615
- end
7616
- ----
7617
-
7618
- .Using the Oga XML adapter
7619
- [source,ruby]
7620
- ----
7621
- require 'lutaml/model'
7622
-
7623
- Lutaml::Model::Config.configure do |config|
7624
- require 'lutaml/model/xml_adapter/oga_adapter'
7625
- config.xml_adapter = Lutaml::Model::XmlAdapter::OgaAdapter
7626
- end
7627
- ----
7628
-
7629
- .Using the Ox XML adapter
9684
+ .Using an XML adapter
7630
9685
  [source,ruby]
7631
9686
  ----
7632
9687
  require 'lutaml/model'
7633
9688
 
7634
9689
  Lutaml::Model::Config.configure do |config|
7635
- require 'lutaml/model/xml_adapter/ox_adapter'
7636
- 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
7637
9695
  end
7638
9696
  ----
7639
9697
 
@@ -7647,14 +9705,13 @@ YAML::
7647
9705
  The Psych YAML parser and emitter for Ruby.
7648
9706
  Included in the Ruby standard library.
7649
9707
 
7650
- .Using the YAML adapter
9708
+ .Using a YAML adapter
7651
9709
  [source,ruby]
7652
9710
  ----
7653
9711
  require 'lutaml/model'
7654
9712
 
7655
9713
  Lutaml::Model::Config.configure do |config|
7656
- require 'lutaml/model/yaml_adapter/standard_yaml_adapter'
7657
- config.yaml_adapter = Lutaml::Model::YamlAdapter::StandardYamlAdapter
9714
+ config.yaml_adapter = :standard_yaml
7658
9715
  end
7659
9716
  ----
7660
9717
 
@@ -7673,27 +9730,18 @@ MultiJson::
7673
9730
  A gem that provides a common interface to multiple JSON libraries.
7674
9731
  Requires the `multi_json` gem.
7675
9732
 
7676
- .Using the JSON adapter
9733
+ .Using a JSON adapter
7677
9734
  [source,ruby]
7678
9735
  ----
7679
9736
  require 'lutaml/model'
7680
9737
 
7681
9738
  Lutaml::Model::Config.configure do |config|
7682
- require 'lutaml/model/json_adapter/standard_json_adapter'
7683
- config.json_adapter = Lutaml::Model::JsonAdapter::StandardJsonAdapter
9739
+ config.json_adapter = :standard_json
9740
+ # or
9741
+ config.json_adapter = :multi_json
7684
9742
  end
7685
9743
  ----
7686
9744
 
7687
- .Using the MultiJson adapter
7688
- [source,ruby]
7689
- ----
7690
- require 'lutaml/model'
7691
-
7692
- Lutaml::Model::Config.configure do |config|
7693
- require 'lutaml/model/json_adapter/multi_json_adapter'
7694
- config.json_adapter = Lutaml::Model::JsonAdapter::MultiJsonAdapter
7695
- end
7696
- ----
7697
9745
 
7698
9746
  === TOML
7699
9747
 
@@ -7712,33 +9760,82 @@ additional features.
7712
9760
  Requires the `tomlib` gem.
7713
9761
 
7714
9762
 
7715
- .Using the Toml-rb adapter
9763
+ .Using a TOML adapter
7716
9764
  [source,ruby]
7717
9765
  ----
7718
9766
  require 'lutaml/model'
7719
9767
 
7720
9768
  Lutaml::Model::Config.configure do |config|
7721
- require 'lutaml/model/toml_adapter/toml_rb_adapter'
7722
- config.toml_adapter = Lutaml::Model::TomlAdapter::TomlRbAdapter
9769
+ config.toml_adapter = :toml_rb
9770
+ # or
9771
+ config.toml_adapter = :tomlib
7723
9772
  end
7724
9773
  ----
7725
9774
 
7726
- .Using the Tomlib adapter
7727
- [source,ruby]
7728
- ----
7729
- require 'lutaml/model'
7730
-
7731
- Lutaml::Model::Config.configure do |config|
7732
- config.toml_adapter = Lutaml::Model::TomlAdapter::TomlibAdapter
7733
- require 'lutaml/model/toml_adapter/tomlib_adapter'
7734
- end
7735
- ----
7736
9775
 
7737
9776
  [[custom-adapters]]
7738
- === 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:
7739
9799
 
7740
- Lutaml::Model provides a flexible system for creating custom adapters to handle different data formats.
7741
- For more information See link:docs/custom_adapters.adoc[Custom Adapters Guide]
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
+ ```
7742
9839
 
7743
9840
  == Comparison with Shale
7744
9841
 
@@ -7853,6 +9950,11 @@ data models.
7853
9950
  | No.
7854
9951
  | Lutaml::Model supports attribute extraction from key-value data models.
7855
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
+
7856
9958
  |===
7857
9959
 
7858
9960