lutaml-model 0.5.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +58 -21
  3. data/Gemfile +1 -0
  4. data/README.adoc +1112 -264
  5. data/lib/lutaml/model/attribute.rb +33 -10
  6. data/lib/lutaml/model/choice.rb +56 -0
  7. data/lib/lutaml/model/config.rb +1 -0
  8. data/lib/lutaml/model/error/choice_lower_bound_error.rb +9 -0
  9. data/lib/lutaml/model/error/choice_upper_bound_error.rb +9 -0
  10. data/lib/lutaml/model/error/import_model_with_root_error.rb +9 -0
  11. data/lib/lutaml/model/error/incorrect_sequence_error.rb +9 -0
  12. data/lib/lutaml/model/error/invalid_choice_range_error.rb +20 -0
  13. data/lib/lutaml/model/error/no_root_mapping_error.rb +9 -0
  14. data/lib/lutaml/model/error/no_root_namespace_error.rb +9 -0
  15. data/lib/lutaml/model/error/unknown_sequence_mapping_error.rb +9 -0
  16. data/lib/lutaml/model/error.rb +8 -0
  17. data/lib/lutaml/model/json_adapter/standard_json_adapter.rb +6 -1
  18. data/lib/lutaml/model/key_value_mapping.rb +3 -1
  19. data/lib/lutaml/model/key_value_mapping_rule.rb +4 -2
  20. data/lib/lutaml/model/liquefiable.rb +59 -0
  21. data/lib/lutaml/model/mapping_hash.rb +1 -1
  22. data/lib/lutaml/model/mapping_rule.rb +15 -2
  23. data/lib/lutaml/model/schema/xml_compiler.rb +68 -26
  24. data/lib/lutaml/model/schema_location.rb +7 -0
  25. data/lib/lutaml/model/sequence.rb +71 -0
  26. data/lib/lutaml/model/serialize.rb +125 -35
  27. data/lib/lutaml/model/type/decimal.rb +0 -4
  28. data/lib/lutaml/model/type/time.rb +3 -3
  29. data/lib/lutaml/model/utils.rb +19 -15
  30. data/lib/lutaml/model/validation.rb +12 -1
  31. data/lib/lutaml/model/version.rb +1 -1
  32. data/lib/lutaml/model/xml_adapter/builder/oga.rb +10 -7
  33. data/lib/lutaml/model/xml_adapter/builder/ox.rb +20 -13
  34. data/lib/lutaml/model/xml_adapter/element.rb +32 -0
  35. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +8 -8
  36. data/lib/lutaml/model/xml_adapter/oga/element.rb +14 -13
  37. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +86 -19
  38. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +19 -15
  39. data/lib/lutaml/model/xml_adapter/xml_document.rb +74 -13
  40. data/lib/lutaml/model/xml_adapter/xml_element.rb +57 -3
  41. data/lib/lutaml/model/xml_mapping.rb +49 -7
  42. data/lib/lutaml/model/xml_mapping_rule.rb +8 -3
  43. data/lib/lutaml/model.rb +1 -0
  44. data/lutaml-model.gemspec +5 -0
  45. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +75 -0
  46. data/spec/ceramic_spec.rb +39 -0
  47. data/spec/fixtures/ceramic.rb +23 -0
  48. data/spec/fixtures/xml/address_example_260.xsd +9 -0
  49. data/spec/fixtures/xml/user.xsd +10 -0
  50. data/spec/lutaml/model/cdata_spec.rb +4 -5
  51. data/spec/lutaml/model/choice_spec.rb +168 -0
  52. data/spec/lutaml/model/collection_spec.rb +1 -1
  53. data/spec/lutaml/model/custom_model_spec.rb +6 -7
  54. data/spec/lutaml/model/custom_serialization_spec.rb +74 -2
  55. data/spec/lutaml/model/defaults_spec.rb +3 -1
  56. data/spec/lutaml/model/delegation_spec.rb +7 -5
  57. data/spec/lutaml/model/enum_spec.rb +35 -0
  58. data/spec/lutaml/model/group_spec.rb +160 -0
  59. data/spec/lutaml/model/inheritance_spec.rb +25 -0
  60. data/spec/lutaml/model/liquefiable_spec.rb +121 -0
  61. data/spec/lutaml/model/mixed_content_spec.rb +80 -41
  62. data/spec/lutaml/model/multiple_mapping_spec.rb +22 -10
  63. data/spec/lutaml/model/schema/xml_compiler_spec.rb +218 -25
  64. data/spec/lutaml/model/sequence_spec.rb +216 -0
  65. data/spec/lutaml/model/transformation_spec.rb +230 -0
  66. data/spec/lutaml/model/type_spec.rb +138 -31
  67. data/spec/lutaml/model/utils_spec.rb +32 -0
  68. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +11 -7
  69. data/spec/lutaml/model/xml_mapping_rule_spec.rb +51 -0
  70. data/spec/lutaml/model/xml_mapping_spec.rb +143 -112
  71. metadata +67 -2
data/README.adoc CHANGED
@@ -8,19 +8,27 @@ image:https://img.shields.io/gem/v/lutaml-model.svg[RubyGems Version]
8
8
 
9
9
  == Purpose
10
10
 
11
- Lutaml::Model is a lightweight library for serializing and deserializing Ruby
12
- objects to and from various formats such as JSON, XML, YAML, and TOML. It uses
13
- an adapter pattern to support multiple libraries for each format, providing
14
- flexibility and extensibility for your data modeling needs.
11
+ Lutaml::Model is the Ruby implementation of the LutaML modeling methodology,
12
+ for:
15
13
 
16
- NOTE: The Lutaml::Model modeling Ruby API is designed to be mostly compatible
17
- with the data modeling API of https://www.shalerb.org[Shale], a data modeller
18
- for Ruby.
19
- Lutaml::Model is meant to address advanced needs not currently addressed by
20
- Shale.
14
+ * creating information models in the LutaML language (or its Ruby DSL)
15
+ * serializing and deserializing LutaML information models
16
+ * accessing data instances of LutaML information models
17
+ * documenting LutaML information models
21
18
 
22
- NOTE: Instructions on how to migrate from Shale to Lutaml::Model are provided in
23
- <<migrate-from-shale>>.
19
+ It provides simple, flexible and comprehensive mechanisms for defining
20
+ information models with attributes and types, and the serialization of them
21
+ to/from serialization formats including JSON, XML, YAML, and TOML.
22
+
23
+ For serialization formats, it uses an adapter pattern to support multiple
24
+ libraries for each format, providing flexibility and extensibility for your data
25
+ modeling needs.
26
+
27
+ NOTE: The Lutaml::Model modeling Ruby DSL was originally designed to be mostly
28
+ compatible with the data modeling DSL of https://www.shalerb.org[Shale], a data
29
+ modeller for Ruby. Lutaml::Model is meant to address advanced needs not
30
+ currently addressed by Shale. Instructions on how to migrate from Shale to
31
+ Lutaml::Model are provided in <<migrate-from-shale>>.
24
32
 
25
33
 
26
34
  == Features
@@ -96,38 +104,38 @@ Studio (Model)
96
104
  .Modeling relationships of a LutaML Model to serialization models
97
105
  [source]
98
106
  ----
99
- ╔═══════════════════════╗ ╔════════════════════════════╗
100
- ║ LutaML Core Model ║ ║ Serialization Models ║
101
- ╚═══════════════════════╝ ╚════════════════════════════╝
102
-
103
- ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
104
- ┆ Model ┆ ┆ XML Model ┆
105
- ┆ │ ┆ ┌────────────────┐ ┆ │ ┆
106
- ┆ ┌────────┴──┐ ┆ │ │ ┆ ┌──────┴──────┐ ┆
107
- ┆ │ │ ┆ │ Model │ ┆ │ │ ┆
108
- ┆ Models Value Types ┆──►│ Transformation │ ┆ Models Value Types ┆
109
- ┆ │ │ ┆ │ & │ ┆ │ │ ┆
110
- ┆ │ │ ┆ │ Mapping Rules │ ┆ │ │ ┆
111
- ┆ │ ┌──────┴──┐ ┆ │ │ ┆ ┌────┴────┐ ┌─┴─┐ ┆
112
- ┆ │ │ │ ┆ └────────────────┘ ┆ │ │ │ │ ┆
113
- ┆ │ String Integer ┆ │ ┆ Element Value xs:string ┆
114
- ┆ │ Date Float ┆ │ ┆ Attribute Type xs:date ┆
115
- ┆ │ Time Boolean ┆ ├──────► ┆ xs:boolean ┆
116
- ┆ │ ┆ │ ┆ xs:anyURI ┆
117
- ┆ └──────┐ ┆ │ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
107
+ ╔═══════════════════════╗ ╔════════════════════════════╗
108
+ ║ LutaML Core Model ║ ║ Serialization Models ║
109
+ ╚═══════════════════════╝ ╚════════════════════════════╝
110
+
111
+ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
112
+ ┆ Model ┆ ┆ XML Model ┆
113
+ ┆ │ ┆ ┌────────────────┐ ┆ │ ┆
114
+ ┆ ┌────────┴──┐ ┆ │ │ ┆ ┌──────┴──────┐ ┆
115
+ ┆ │ │ ┆ │ Model │ ┆ │ │ ┆
116
+ ┆ Models Value Types ┆──►│ Transformation │ ┆ Models Value Types ┆
117
+ ┆ │ │ ┆ │ & │ ┆ │ │ ┆
118
+ ┆ │ │ ┆ │ Mapping Rules │ ┆ │ │ ┆
119
+ ┆ │ ┌──────┴──┐ ┆ │ │ ┆ ┌────┴────┐ ┌─┴─┐ ┆
120
+ ┆ │ │ │ ┆ └────────────────┘ ┆ │ │ │ │ ┆
121
+ ┆ │ String Integer ┆ │ ┆ Element Value xs:string ┆
122
+ ┆ │ Date Float ┆ │ ┆ Attribute Type xs:date ┆
123
+ ┆ │ Time Boolean ┆ ├──────────►┆ xs:boolean ┆
124
+ ┆ │ ┆ │ ┆ xs:anyURI ┆
125
+ ┆ └──────┐ ┆ │ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
118
126
  ┆ │ ┆ │
119
- ┆ Contains ┆ │ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
120
- ┆ more Models ┆ │ ┆ JSON Model ┆
121
- ┆ (recursive) ┆ │ ┆ │ ┆
122
- ┆ ┆ │ ┆ ┌──────┴──────┐ ┆
123
- ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯ └───────► ┆ │ │ ┆
124
- ┆ Models Value Types ┆
125
- ┆ │ │ ┆
126
- ┆ │ │ ┆
127
- ┆ ┌────┴───┐ ┌───┴──┐ ┆
128
- ┆ │ │ │ │ ┆
129
- ┆ object array number string ┆
130
- ┆ value boolean null ┆
127
+ ┆ Contains ┆ │ ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
128
+ ┆ more Models ┆ │ ┆ JSON Model ┆
129
+ ┆ (recursive) ┆ │ ┆ │ ┆
130
+ ┆ ┆ │ ┆ ┌──────┴──────┐ ┆
131
+ ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯ └──────────►┆ │ │ ┆
132
+ ┆ Models Value Types ┆
133
+ ┆ │ │ ┆
134
+ ┆ │ │ ┆
135
+ ┆ ┌────┴───┐ ┌───┴──┐ ┆
136
+ ┆ │ │ │ │ ┆
137
+ ┆ object array number string ┆
138
+ ┆ value boolean null ┆
131
139
  ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
132
140
  ----
133
141
 
@@ -635,6 +643,149 @@ end
635
643
  ====
636
644
 
637
645
 
646
+ === Sequence within XmlMapping
647
+
648
+ The sequence option enforces that the defined components must appear in a specified order.
649
+
650
+ NOTE: `sequence` only works within XML and only supports `map_element` mappings.
651
+
652
+ .Using the `sequence` keyword to define a set of elements in desired order.
653
+ [example]
654
+ ====
655
+ [source,ruby]
656
+ ----
657
+ class Kiln < Lutaml::Model::Serializable
658
+ attribute :id, :string
659
+ attribute :name, :string
660
+ attribute :type, :string
661
+ attribute :color, :string
662
+
663
+ xml do
664
+ sequence do
665
+ map_element :id, to: :id
666
+ map_element :name, to: :name
667
+ map_element :type, to: :type
668
+ map_element :color, to: :color
669
+ end
670
+ end
671
+ end
672
+
673
+ class KilnCollection < Lutaml::Model::Serializable
674
+ attribute :kiln, Kiln, collection: 1..2
675
+
676
+ xml do
677
+ root "collection"
678
+ map_element "kiln", to: :kiln
679
+ end
680
+ end
681
+ ----
682
+
683
+ [source,ruby]
684
+ ----
685
+ > parsed = Kiln.from_xml("<Kiln> <id>1</id> <name>Nick</name> <type>Hard</type> <color>Black</color> </Kiln>")
686
+ > # parsed.not_to raise_error
687
+ ----
688
+ ====
689
+
690
+
691
+ === Reusable Classes with Import
692
+
693
+ Lutaml lets you create reusable element and attribute collections using `no_root`. These can be imported into other models using:
694
+
695
+ - `import_model`: imports both attributes and mappings
696
+ - `import_model_attributes`: imports only attributes
697
+ - `import_model_mappings`: imports only mappings
698
+
699
+ NOTE: This feature works with XML. Import order determines how elements and attributes are overwritten.
700
+
701
+ [example]
702
+ ====
703
+ [source,ruby]
704
+ ----
705
+ class GroupOfItems < Lutaml::Model::Serializable
706
+ attribute :name, :string
707
+ attribute :type, :string
708
+ attribute :code, :string
709
+
710
+ xml do
711
+ no_root
712
+ sequence do
713
+ map_element "name", to: :name
714
+ map_element "type", to: :type, namespace: "http://www.example.com", prefix: "ex1"
715
+ end
716
+ map_attribute "code", to: :code
717
+ end
718
+ end
719
+
720
+ class ComplexType < Lutaml::Model::Serializable
721
+ attribute :tag, AttributeValueType
722
+ attribute :content, :string
723
+ attribute :group, :string
724
+ import_model_attributes GroupOfItems
725
+
726
+ xml do
727
+ root "GroupOfItems"
728
+ map_attribute "tag", to: :tag
729
+ map_content to: :content
730
+ map_element :group, to: :group
731
+ import_model_mappings GroupOfItems
732
+ end
733
+ end
734
+
735
+ class SimpleType < Lutaml::Model::Serializable
736
+ import_model GroupOfItems
737
+ end
738
+
739
+ class GenericType < Lutaml::Model::Serializable
740
+ import_model_mappings GroupOfItems
741
+ end
742
+ ----
743
+
744
+ [source,xml]
745
+ ----
746
+ <GroupOfItems xmlns:ex1="http://www.example.com">
747
+ <name>Name</name>
748
+ <ex1:type>Type</ex1:type>
749
+ </GroupOfItems>
750
+ ----
751
+
752
+ [source,ruby]
753
+ ----
754
+ > parsed = GroupOfItems.from_xml(xml)
755
+ > # Lutaml::Model::NoRootMappingError: "GroupOfItems has `no_root`, it allowed only for reusable models"
756
+ ----
757
+
758
+ NOTE: Models with `no_root` can only be parsed through **Parent Models**. Direct calling `from_xml` will raise `NoRootMappingError`.
759
+ And if `namespace` is defined with `no_root`, `NoRootNamespaceError` will raise.
760
+
761
+ ====
762
+
763
+
764
+ === Choice
765
+
766
+ The `choice` option ensures that elements from the specified range are included.
767
+
768
+ NOTE: Attribute-level definitions are supported. This can be used with both key_value and xml mappings.
769
+
770
+ .Using the `choice` option to define a set of attributes with a range.
771
+ [example]
772
+ ====
773
+ [source,ruby]
774
+ ----
775
+ class Studio < Lutaml::Model::Serializable
776
+ choice(min: 1, max: 3) do
777
+ choice(min: 1, max: 2) do
778
+ attribute :prefix, :string
779
+ attribute :forename, :string
780
+ end
781
+
782
+ attribute :completeName, :string
783
+ end
784
+ end
785
+ ----
786
+ ====
787
+
788
+
638
789
  === Attribute value validation
639
790
 
640
791
  ==== General
@@ -784,7 +935,7 @@ end
784
935
 
785
936
 
786
937
 
787
- === Attribute value default and rendering defaults
938
+ === Attribute value defaults
788
939
 
789
940
  Specify default values for attributes using the `default` option.
790
941
  The `default` option can be set to a value or a lambda that returns a value.
@@ -820,76 +971,6 @@ end
820
971
  The "default behavior" (pun intended) is to not render a default value if
821
972
  the current value is the same as the default value.
822
973
 
823
- In certain cases, it is necessary to render the default value even if the
824
- current value is the same as the default value. This can be achieved by setting
825
- the `render_default` option to `true`.
826
-
827
- Syntax:
828
-
829
- [source,ruby]
830
- ----
831
- attribute :name_of_attribute, Type, default: -> { value }, render_default: true
832
- ----
833
-
834
- .Using the `render_default` option to force encoding the default value
835
- [example]
836
- ====
837
- [source,ruby]
838
- ----
839
- class Glaze < Lutaml::Model::Serializable
840
- attribute :color, :string, default: -> { 'Clear' }
841
- attribute :opacity, :string, default: -> { 'Opaque' }
842
- attribute :temperature, :integer, default: -> { 1050 }
843
- attribute :firing_time, :integer, default: -> { 60 }
844
-
845
- xml do
846
- root "glaze"
847
- map_element 'color', to: :color
848
- map_element 'opacity', to: :opacity, render_default: true
849
- map_attribute 'temperature', to: :temperature
850
- map_attribute 'firingTime', to: :firing_time, render_default: true
851
- end
852
-
853
- json do
854
- map 'color', to: :color
855
- map 'opacity', to: :opacity, render_default: true
856
- map 'temperature', to: :temperature
857
- map 'firingTime', to: :firing_time, render_default: true
858
- end
859
- end
860
- ----
861
- ====
862
-
863
- .Attributes with `render_default: true` are rendered when the value is identical to the default
864
- [example]
865
- ====
866
- [source,ruby]
867
- ----
868
- > glaze_new = Glaze.new
869
- > puts glaze_new.to_xml
870
- # <glaze firingTime="60">
871
- # <opacity>Opaque</opacity>
872
- # </glaze>
873
- > puts glaze_new.to_json
874
- # {"firingTime":60,"opacity":"Opaque"}
875
- ----
876
- ====
877
-
878
- .Attributes with `render_default: true` with non-default values are rendered
879
- [example]
880
- ====
881
- [source,ruby]
882
- ----
883
- > glaze = Glaze.new(color: 'Celadon', opacity: 'Semitransparent', temperature: 1300, firing_time: 90)
884
- > puts glaze.to_xml
885
- # <glaze color="Celadon" temperature="1300" firingTime="90">
886
- # <opacity>Semitransparent</opacity>
887
- # </glaze>
888
- > puts glaze.to_json
889
- # {"color":"Celadon","temperature":1300,"firingTime":90,"opacity":"Semitransparent"}
890
- ----
891
- ====
892
-
893
974
 
894
975
 
895
976
  === Attribute as raw string
@@ -950,7 +1031,7 @@ defining serialization and deserialization mappings.
950
1031
  Serialization model mappings are defined under the `xml`, `json`, `yaml`, and
951
1032
  `toml` blocks.
952
1033
 
953
- .Using the `xml`, `json`, `yaml`, and `toml` blocks to define serialization mappings
1034
+ .Using the `xml`, `json`, `yaml`, `toml` and `key_value` blocks to define serialization mappings
954
1035
  [source,ruby]
955
1036
  ----
956
1037
  class Example < Lutaml::Model::Serializable
@@ -969,6 +1050,10 @@ class Example < Lutaml::Model::Serializable
969
1050
  toml do
970
1051
  # ...
971
1052
  end
1053
+
1054
+ key_value do
1055
+ # ...
1056
+ end
972
1057
  end
973
1058
  ----
974
1059
 
@@ -1376,72 +1461,15 @@ The following class will parse the XML snippet below:
1376
1461
 
1377
1462
  [source,ruby]
1378
1463
  ----
1379
- class Example < Lutaml::Model::Serializable
1380
- attribute :name, :string
1381
- attribute :description, :string
1382
- attribute :value, :integer
1383
-
1384
- xml do
1385
- root 'example'
1386
- map_element 'name', to: :name
1387
- map_attribute 'value', to: :value
1388
- map_content to: :description
1389
- end
1390
- end
1391
- ----
1392
-
1393
- [source,xml]
1394
- ----
1395
- <example value="12"><name>John Doe</name> is my moniker.</example>
1396
- ----
1397
-
1398
- [source,ruby]
1399
- ----
1400
- > Example.from_xml(xml)
1401
- > #<Example:0x0000000104ac7240 @name="John Doe", @description=" is my moniker.", @value=12>
1402
- > Example.new(name: "John Doe", description: " is my moniker.", value: 12).to_xml
1403
- > #<example value="12"><name>John Doe</name> is my moniker.</example>
1404
- ----
1405
- ====
1406
-
1407
-
1408
- ==== Encoding Options in XmlAdapter
1409
-
1410
- XmlAdapter supports the encoding in the following ways:
1411
-
1412
- . When encoding is not passed in to_xml:
1413
- ** Default encoding is UTF-8.
1414
-
1415
- . When encoding is explicitly passed nil:
1416
- ** Encoding will be nil, show the HexCode(Nokogiri) or ASCII-8bit(Ox).
1417
-
1418
- . When encoding is passed with some option:
1419
- ** Encoding option will be selected as passed.
1420
-
1421
-
1422
- Syntax:
1423
-
1424
- [source,ruby]
1425
- ----
1426
- Example.new(description: " ∑ is my ∏ moniker µ.").to_xml
1427
- Example.new(description: " ∑ is my ∏ moniker µ.").to_xml(encoding: nil)
1428
- Example.new(description: " ∑ is my ∏ moniker µ.").to_xml(encoding: "ASCII")
1429
- ----
1430
-
1431
- [example]
1432
- ====
1433
- The following class will parse the XML snippet below:
1434
-
1435
- [source,ruby]
1436
- ----
1437
- class Example < Lutaml::Model::Serializable
1464
+ class Ceramic < Lutaml::Model::Serializable
1438
1465
  attribute :name, :string
1439
1466
  attribute :description, :string
1440
- attribute :value, :integer
1467
+ attribute :temperature, :integer
1441
1468
 
1442
1469
  xml do
1443
- root 'example'
1470
+ root 'ceramic'
1444
1471
  map_element 'name', to: :name
1472
+ map_attribute 'temperature', to: :temperature
1445
1473
  map_content to: :description
1446
1474
  end
1447
1475
  end
@@ -1449,21 +1477,15 @@ end
1449
1477
 
1450
1478
  [source,xml]
1451
1479
  ----
1452
- <example><name>John &#x0026; Doe</name> &#x2211; is my &#x220F; moniker &#xB5;.</example>
1480
+ <ceramic temperature="1200"><name>Porcelain Vase</name> with celadon glaze.</ceramic>
1453
1481
  ----
1454
1482
 
1455
1483
  [source,ruby]
1456
1484
  ----
1457
- > Example.from_xml(xml)
1458
- > #<Example:0x0000000104ac7240 @name="John & Doe", @description=" is my ∏ moniker µ.">
1459
- > Example.new(name: "John & Doe", description: " is my moniker µ.").to_xml
1460
- > #<example><name>John &amp; Doe</name> is my ∏ moniker µ.</example>
1461
-
1462
- > Example.new(name: "John & Doe", description: " ∑ is my ∏ moniker µ.").to_xml(encoding: nil)
1463
- > #<example><name>John &amp; Doe</name> &#x2211; is my &#x220F; moniker &#xB5;.</example>
1464
-
1465
- > Example.new(name: "John & Doe", description: " ∑ is my ∏ moniker µ.").to_xml(encoding: "ASCII")
1466
- > #<example><name>John &amp; Doe</name> &#8721; is my &#8719; moniker &#181;.</example>
1485
+ > Ceramic.from_xml(xml)
1486
+ > #<Ceramic:0x0000000104ac7240 @name="Porcelain Vase", @description=" with celadon glaze.", @temperature=1200>
1487
+ > Ceramic.new(name: "Porcelain Vase", description: " with celadon glaze.", temperature: 1200).to_xml
1488
+ > #<ceramic temperature="1200"><name>Porcelain Vase</name> with celadon glaze.</ceramic>
1467
1489
  ----
1468
1490
  ====
1469
1491
 
@@ -1765,6 +1787,99 @@ end
1765
1787
  // TODO: How to create mixed content from `#new`?
1766
1788
 
1767
1789
 
1790
+ [[ordered-content]]
1791
+ ==== Ordered content
1792
+
1793
+ `ordered: true` maintains the order of **XML Elements**, while `mixed: true` preserves the order of **XML Elements and Content**.
1794
+
1795
+ NOTE: When both options are used, `mixed: true` takes precedence.
1796
+
1797
+ To specify ordered content, the `ordered: true` option needs to be set at the
1798
+ `xml` block's `root` method.
1799
+
1800
+ Syntax:
1801
+
1802
+ [source,ruby]
1803
+ ----
1804
+ xml do
1805
+ root 'xml_element_name', ordered: true
1806
+ end
1807
+ ----
1808
+
1809
+ .Applying `ordered` to treat root as ordered content
1810
+ [example]
1811
+ ====
1812
+
1813
+ [source,ruby]
1814
+ ----
1815
+ class RootOrderedContent < Lutaml::Model::Serializable
1816
+ attribute :bold, :string
1817
+ attribute :italic, :string
1818
+ attribute :underline, :string
1819
+
1820
+ xml do
1821
+ root "RootOrderedContent", ordered: true
1822
+ map_element :bold, to: :bold
1823
+ map_element :italic, to: :italic
1824
+ map_element :underline, to: :underline
1825
+ end
1826
+ end
1827
+ ----
1828
+
1829
+ [source,xml]
1830
+ ----
1831
+ <RootOrderedContent>
1832
+ <underline>Moon</underline>
1833
+ <italic>384,400 km</italic>
1834
+ <bold>bell</bold>
1835
+ </RootOrderedContent>
1836
+ ----
1837
+
1838
+ [source,ruby]
1839
+ ----
1840
+ > instance = RootOrderedContent.from_xml(xml)
1841
+ > #<RootOrderedContent:0x0000000104ac7240 @bold="bell", @italic="384,400 km", @underline="Moon">
1842
+ > instance.to_xml
1843
+ > #<RootOrderedContent><underline>Moon</underline><italic>384,400 km</italic><bold>bell</bold></RootOrderedContent>
1844
+ ----
1845
+
1846
+ **Without Ordered True:**
1847
+
1848
+ [source,ruby]
1849
+ ----
1850
+ class RootOrderedContent < Lutaml::Model::Serializable
1851
+ attribute :bold, :string
1852
+ attribute :italic, :string
1853
+ attribute :underline, :string
1854
+
1855
+ xml do
1856
+ root "RootOrderedContent"
1857
+ map_element :bold, to: :bold
1858
+ map_element :italic, to: :italic
1859
+ map_element :underline, to: :underline
1860
+ end
1861
+ end
1862
+ ----
1863
+
1864
+ [source,xml]
1865
+ ----
1866
+ <RootOrderedContent>
1867
+ <underline>Moon</underline>
1868
+ <italic>384,400 km</italic>
1869
+ <bold>bell</bold>
1870
+ </RootOrderedContent>
1871
+ ----
1872
+
1873
+ [source,ruby]
1874
+ ----
1875
+ > instance = RootOrderedContent.from_xml(xml)
1876
+ > #<RootOrderedContent:0x0000000104ac7240 @bold="bell", @italic="384,400 km", @underline="Moon">
1877
+ > instance.to_xml
1878
+ > #<RootOrderedContent>\n <bold>bell</bold>\n <italic>384,400 km</italic>\n <underline>Moon</underline>\n</RootOrderedContent>
1879
+ ----
1880
+ ====
1881
+
1882
+
1768
1883
  [[xml-schema-location]]
1769
1884
  ==== Automatic support of `xsi:schemaLocation`
1770
1885
 
@@ -1803,88 +1918,427 @@ The following snippet shows how `xsi:schemaLocation` is used in an XML document:
1803
1918
  </cera:Ceramic>
1804
1919
  ----
1805
1920
 
1806
- LutaML::Model supports the `xsi:schemaLocation` attribute in all XML
1807
- serializations by default, through the `schema_location` attribute on the model
1808
- instance object.
1921
+ LutaML::Model supports the `xsi:schemaLocation` attribute in all XML
1922
+ serializations by default, through the `schema_location` attribute on the model
1923
+ instance object.
1924
+
1925
+ .Retrieving and setting the `xsi:schemaLocation` attribute in XML serialization
1926
+ [example]
1927
+ ====
1928
+ In this example, the `xsi:schemaLocation` attribute will be automatically
1929
+ supplied without the explicit need to define in the model, and allows for
1930
+ round-trip serialization.
1931
+
1932
+ [source,ruby]
1933
+ ----
1934
+ class Ceramic < Lutaml::Model::Serializable
1935
+ attribute :type, :string
1936
+ attribute :glaze, :string
1937
+ attribute :color, :string
1938
+
1939
+ xml do
1940
+ root 'Ceramic'
1941
+ namespace 'http://example.com/ceramic', 'cera'
1942
+ map_element 'Type', to: :type, namespace: :inherit
1943
+ map_element 'Glaze', to: :glaze
1944
+ map_attribute 'color', to: :color, namespace: 'http://example.com/color', prefix: 'clr'
1945
+ end
1946
+ end
1947
+
1948
+ xml_content = <<~HERE
1949
+ <cera:Ceramic
1950
+ xmlns:cera="http://example.com/ceramic"
1951
+ xmlns:clr="http://example.com/color"
1952
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1953
+ clr:color="navy-blue"
1954
+ xsi:schemaLocation="
1955
+ http://example.com/ceramic http://example.com/ceramic.xsd
1956
+ http://example.com/color http://example.com/color.xsd
1957
+ ">
1958
+ <cera:Type>Porcelain</cera:Type>
1959
+ <Glaze>Clear</Glaze>
1960
+ </cera:Ceramic>
1961
+ HERE
1962
+ ----
1963
+
1964
+ [source,ruby]
1965
+ ----
1966
+ > c = Ceramic.from_xml(xml_content)
1967
+ =>
1968
+ #<Ceramic:0x00000001222bdd60
1969
+ ...
1970
+ > schema_loc = c.schema_location
1971
+ #<Lutaml::Model::SchemaLocation:0x0000000122773760
1972
+ ...
1973
+ > schema_loc
1974
+ =>
1975
+ #<Lutaml::Model::SchemaLocation:0x0000000122773760
1976
+ @namespace="http://www.w3.org/2001/XMLSchema-instance",
1977
+ @original_schema_location="http://example.com/ceramic http://example.com/ceramic.xsd http://example.com/color http://example.com/color.xsd",
1978
+ @prefix="xsi",
1979
+ @schema_location=
1980
+ [#<Lutaml::Model::Location:0x00000001222bd018 @location="http://example.com/ceramic.xsd", @namespace="http://example.com/ceramic">,
1981
+ #<Lutaml::Model::Location:0x00000001222bcfc8 @location="http://example.com/color.xsd", @namespace="http://example.com/color">]>
1982
+ > new_c = Ceramic.new(type: "Porcelain", glaze: "Clear", color: "navy-blue", schema_location: schema_loc).to_xml
1983
+ > puts new_c
1984
+ # <cera:Ceramic
1985
+ # xmlns:cera="http://example.com/ceramic"
1986
+ # xmlns:clr="http://example.com/color"
1987
+ # xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1988
+ # clr:color="navy-blue"
1989
+ # xsi:schemaLocation="
1990
+ # http://example.com/ceramic http://example.com/ceramic.xsd
1991
+ # http://example.com/color http://example.com/color.xsd
1992
+ # ">
1993
+ # <cera:Type>Porcelain</cera:Type>
1994
+ # <cera:Glaze>Clear</cera:Glaze>
1995
+ # </cera:Ceramic>
1996
+ ----
1997
+ ====
1998
+
1999
+ NOTE: For details on `xsi:schemaLocation`, please refer to the
2000
+ https://www.w3.org/TR/xmlschema-1/#xsi_schemaLocation[W3C XML standard].
2001
+
2002
+
2003
+
2004
+ ==== Character encoding
2005
+
2006
+ ===== General
2007
+
2008
+ Lutaml::Model XML adapters use a default encoding of `UTF-8` for both input and
2009
+ output.
2010
+
2011
+ Serialization data to be parsed (deserialization) and serialization data to be
2012
+ exported (serialization) may be in a different character encoding than the
2013
+ default encoding used by the Lutaml::Model XML adapter. This mismatch may lead
2014
+ to incorrect data reading or incompatibilities when exporting data.
2015
+
2016
+ The possible values for setting character encoding to are:
2017
+
2018
+ * A valid encoding value, e.g. `UTF-8`, `Shift_JIS`, `ASCII`;
2019
+
2020
+ * `nil` to use the default encoding of the adapter. The behavior differs based
2021
+ on the adapter used.
2022
+
2023
+ ** Nokogiri: `UTF-8`. The encoding is set to the default encoding of the Nokogiri library,
2024
+ which is `UTF-8`.
2025
+
2026
+ ** Oga: `UTF-8`. The encoding is set to the default encoding of the Oga library, which
2027
+ uses `UTF-8`.
2028
+
2029
+ ** Ox: `ASCII-8bit`. The encoding is set to the default encoding of the Ox library, which uses
2030
+ `ASCII-8bit`.
2031
+
2032
+ When the `encoding` option is not set, the default encoding of `UTF-8` is
2033
+ used.
2034
+
2035
+
2036
+ ===== Serialization character encoding (exporting)
2037
+
2038
+ ====== General
2039
+
2040
+ There are two ways to set the character encoding of the XML document during
2041
+ serialization:
2042
+
2043
+ Instance setting::
2044
+ Setting the instance-level `encoding` option by setting
2045
+ `ModelClassInstance.encoding('...')`. This setting only affects serialization.
2046
+
2047
+ Per-export setting::
2048
+ Setting the `encoding` option when calling for serialization action using the
2049
+ `ModelClassInstance.to_xml(..., encoding: ...)` method.
2050
+
2051
+ [[encoding-instance-setting]]
2052
+ ====== Instance setting
2053
+
2054
+ The `encoding` value of an instance sets the character encoding of the XML
2055
+ document during serialization.
2056
+
2057
+ Syntax:
2058
+
2059
+ [source,ruby]
2060
+ ----
2061
+ ModelClassInstance.encoding = {encoding_value}
2062
+ ----
2063
+
2064
+ Where,
2065
+
2066
+ `ModelClassInstance`:: An instance of the class that inherits from
2067
+ Lutaml::Model::Serializable.
2068
+ `{encoding_value}`:: The encoding of the output data.
2069
+
2070
+ .Character encoding set to instance is reflected in its serialization output
2071
+ [example]
2072
+ ====
2073
+ [source,ruby]
2074
+ ----
2075
+ class JapaneseCeramic < Lutaml::Model::Serializable
2076
+ attribute :glaze_type, :string
2077
+ attribute :description, :string
2078
+
2079
+ xml do
2080
+ root 'JapaneseCeramic'
2081
+ map_attribute 'glazeType', to: :glaze_type
2082
+ map_element 'description' to: :description
2083
+ end
2084
+ end
2085
+ ----
2086
+
2087
+ [source,ruby]
2088
+ ----
2089
+ # Create a new instance with UTF-8 data
2090
+ > instance = JapaneseCeramic.new(glaze_type: "志野釉", description: "東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)")
2091
+ #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
2092
+
2093
+ # Set character encoding to Shift_JIS
2094
+ > instance.encoding = "Shift_JIS"
2095
+ #=> "Shift_JIS"
2096
+
2097
+ # Serialize the instance
2098
+ > serialization_output = instance.to_xml
2099
+ #=> #<JapaneseCeramic><glazeType>\x{5FD8}\x{91CE}\x{91C9}</glazeType><description>\x{6771}\x{4EAC}\x{56FD}\x{7ACB}\x{535A}\x{7269}\x{9928}\x{30B3}\x{30EC}\x{30AF}\x{30B7}\x{30E7}\x{30F3}\x{306E}\x{7BC0}\x{8336}\x{7897}\x{300C}\x{6A4B}\x{672C}\x{300D}\x{FF08}\x{6853}\x{5C71}\x{6642}\x{4EE3}\x{FF09}</description></JapaneseCeramic>
2100
+
2101
+ # Check character encoding of output
2102
+ > serialization_output.encoding
2103
+ #=> "Shift_JIS"
2104
+ ----
2105
+ ====
2106
+
2107
+
2108
+ ====== Per-export setting
2109
+
2110
+ The `encoding` option is used in the `ModelClass#to_xml(..., encoding: ...)`
2111
+ call to set the character encoding of the XML document during serialization.
2112
+
2113
+ The per-export encoding setting supersedes the instance-level encoding setting.
2114
+
2115
+ Syntax:
2116
+
2117
+ [source,ruby]
2118
+ ----
2119
+ ModelClassInstance.to_xml(encoding: {encoding_value})
2120
+ ----
2121
+
2122
+ Where,
2123
+
2124
+ `ModelClassInstance`:: An instance of the class that inherits from
2125
+ Lutaml::Model::Serializable.
2126
+ `{encoding_value}`:: The encoding of the output data.
2127
+
2128
+
2129
+ [example]
2130
+ ====
2131
+ The following class will parse the XML snippet below:
2132
+
2133
+ [source,ruby]
2134
+ ----
2135
+ class Ceramic < Lutaml::Model::Serializable
2136
+ attribute :potter, :string
2137
+ attribute :description, :string
2138
+ attribute :temperature, :integer
2139
+
2140
+ xml do
2141
+ root 'ceramic'
2142
+ map_element 'potter', to: :potter
2143
+ map_content to: :description
2144
+ end
2145
+ end
2146
+ ----
2147
+
2148
+ [source,xml]
2149
+ ----
2150
+ <ceramic><potter>John &#x0026; Jane</potter> A &#x2211; series of &#x220F; porcelain &#xB5; vases.</ceramic>
2151
+ ----
2152
+
2153
+ [source,ruby]
2154
+ ----
2155
+ # Object with attributes
2156
+ > ceramic_instance = Ceramic.new(potter: "John & Jane", description: " A ∑ series of ∏ porcelain µ vases.")
2157
+ > #<Ceramic:0x0000000104ac7240 @potter="John & Jane", @description=" A ∑ series of ∏ porcelain µ vases.">
2158
+
2159
+ # Parsing the XML snippet with the default encoding of UTF-8
2160
+ > ceramic_parsed = Ceramic.from_xml(xml)
2161
+ > #<Ceramic:0x0000000104ac7242 @potter="John & Jane", @description=" A ∑ series of ∏ porcelain µ vases.">
2162
+
2163
+ # Object with attributes is equal to the parsed object
2164
+ > ceramic_parsed == ceramic_instance
2165
+ > # true
2166
+
2167
+ # Using the default encoding of UTF-8
2168
+ > ceramic_instance.to_xml
2169
+ > #<ceramic><potter>John &amp; Jane</potter> A ∑ series of ∏ porcelain µ vases.</ceramic>
2170
+
2171
+ # Using the default encoding of the adapter, which is UTF-8 in this case
2172
+ > ceramic_instance.to_xml(encoding: nil)
2173
+ > #<ceramic><potter>John &amp; Jane</potter> A &#x2211; series of &#x220F; porcelain &#xB5; vases.</ceramic>
2174
+
2175
+ # Using ASCII encoding
2176
+ > ceramic_instance.to_xml(encoding: "ASCII")
2177
+ > #<ceramic><potter>John &amp; Jane</potter> A &#8721; series of &#8719; porcelain &#181; vases.</ceramic>
2178
+ ----
2179
+ ====
2180
+
2181
+
2182
+ .Character encoding set at `to_xml` overrides instance encoding
2183
+ [example]
2184
+ ====
2185
+ [source,ruby]
2186
+ ----
2187
+ class JapaneseCeramic < Lutaml::Model::Serializable
2188
+ attribute :glaze_type, :string
2189
+ attribute :description, :string
2190
+
2191
+ xml do
2192
+ root 'JapaneseCeramic'
2193
+ map_attribute 'glazeType', to: :glaze_type
2194
+ map_element 'description' to: :description
2195
+ end
2196
+ end
2197
+ ----
2198
+
2199
+ [source,ruby]
2200
+ ----
2201
+ # Create a new instance with UTF-8 data
2202
+ > instance = JapaneseCeramic.new(glaze_type: "志野釉", description: "東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)")
2203
+ #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
2204
+
2205
+ # Set character encoding to Shift_JIS
2206
+ > instance.encoding = "Shift_JIS"
2207
+ #=> "Shift_JIS"
2208
+
2209
+ # Serialize the instance
2210
+ > serialization_output = instance.to_xml(encoding: "UTF-8")
2211
+ #=> #<JapaneseCeramic><glazeType>志野釉</glazeType><description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description></JapaneseCeramic>
2212
+
2213
+ # Check character encoding of output
2214
+ > serialization_output.encoding
2215
+ #=> "UTF-8"
2216
+ ----
2217
+ ====
2218
+
2219
+
2220
+ ===== Deserialization character encoding (parsing)
2221
+
2222
+ The character encoding of the XML document being parsed is specified using the
2223
+ `encoding` option when the `ModelClass.from_{format}(...)` is called.
2224
+
2225
+ Syntax:
2226
+
2227
+ [source,ruby]
2228
+ ----
2229
+ ModelClass.from_{format}(string_in_format, encoding: {encoding_value})
2230
+ ----
2231
+
2232
+ Where,
2233
+
2234
+ `ModelClass`:: The class that inherits from Lutaml::Model::Serializable.
2235
+ `{format}`:: The format of the input data, e.g. `xml`, `json`, `yaml`, `toml`.
2236
+ `string_in_format`:: The input data in the specified format.
2237
+ `{encoding_value}`:: The encoding of the input data.
2238
+
2239
+
2240
+ .Setting the `encoding` option during parsing data not encoded in the default encoding (UTF-8)
2241
+ [example]
2242
+ ====
2243
+ Using the definition of `JapaneseCeramic` at <<encoding-instance-setting>>.
2244
+
2245
+ This XML snippet is in Shift-JIS.
2246
+
2247
+ [source,xml]
2248
+ ----
2249
+ <JapaneseCeramic>
2250
+ <glazeType>\x{5FD8}\x{91CE}\x{91C9}</glazeType>
2251
+ <description>\x{6771}\x{4EAC}\x{56FD}\x{7ACB}\x{535A}\x{7269}\x{9928}\x{30B3}\x{30EC}\x{30AF}\x{30B7}\x{30E7}\x{30F3}\x{306E}\x{7BC0}\x{8336}\x{7897}\x{300C}\x{6A4B}\x{672C}\x{300D}\x{FF08}\x{6853}\x{5C71}\x{6642}\x{4EE3}\x{FF09}</description>
2252
+ </JapaneseCeramic>
2253
+ ----
2254
+
2255
+ [source,ruby]
2256
+ ----
2257
+ # Parse the XML snippet with the encoding of Shift_JIS
2258
+ > instance = JapaneseCeramic.from_xml(xml, encoding: "Shift_JIS")
2259
+ #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
2260
+
2261
+ # Check character encoding of the instance
2262
+ > instance.encoding
2263
+ #=> "Shift_JIS"
2264
+
2265
+ # Serialize the instance using UTF-8
2266
+ > serialization_output = instance.to_xml(encoding: "UTF-8")
2267
+ #=> #<JapaneseCeramic><glazeType>志野釉</glazeType><description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description></JapaneseCeramic>
2268
+ > serialization_output.encoding
2269
+ #=> "UTF-8"
2270
+ ----
2271
+ ====
2272
+
2273
+ .When the `encoding` option is not set, the default encoding of the adapter is used
2274
+ [example]
2275
+ ====
2276
+ Using the definition of `JapaneseCeramic` at <<encoding-instance-setting>>.
2277
+
2278
+ This XML snippet is in UTF-8.
2279
+
2280
+ [source,xml]
2281
+ ----
2282
+ <JapaneseCeramic>
2283
+ <glazeType>志野釉</glazeType>
2284
+ <description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description>
2285
+ </JapaneseCeramic>
2286
+ ----
2287
+
2288
+ In adapters that use a default encoding of `UTF-8`, the content is parsed
2289
+ properly.
2290
+
2291
+ [source,ruby]
2292
+ ----
2293
+ > instance = JapaneseCeramic.from_xml(xml, encoding: nil)
2294
+ #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
2295
+ > instance.encoding
2296
+ #=> "UTF-8"
2297
+ > serialization_output = instance.to_xml
2298
+ #=> #<JapaneseCeramic><glazeType>志野釉</glazeType><description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description></JapaneseCeramic>
2299
+ > serialization_output.encoding
2300
+ #=> "UTF-8"
2301
+ ----
2302
+
2303
+ In adapters that use a default encoding of `ASCII-8bit`, the content becomes
2304
+ malformed.
2305
+
2306
+ [source,ruby]
2307
+ ----
2308
+ > instance = JapaneseCeramic.from_xml(xml, encoding: nil)
2309
+ #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="�菑�", @description="�東京国立博物館コレクションの篠茶碗�橋本�桃山時代�">
2310
+ > instance.encoding
2311
+ #=> "ASCII-8bit"
2312
+ > serialization_output = instance.to_xml
2313
+ #=> #<JapaneseCeramic><glazeType>�菑�</glazeType><description>�東京国立博物館コレクションの篠茶碗�橋本�桃山時代�</description></JapaneseCeramic>
2314
+ > serialization_output.encoding
2315
+ #=> "ASCII-8bit"
2316
+ ----
2317
+ ====
2318
+
1809
2319
 
1810
- .Retrieving and setting the `xsi:schemaLocation` attribute in XML serialization
2320
+ .Using an invalid encoding to deserialize causes data corruption
1811
2321
  [example]
1812
2322
  ====
1813
- In this example, the `xsi:schemaLocation` attribute will be automatically
1814
- supplied without the explicit need to define in the model, and allows for
1815
- round-trip serialization.
1816
-
1817
- [source,ruby]
1818
- ----
1819
- class Ceramic < Lutaml::Model::Serializable
1820
- attribute :type, :string
1821
- attribute :glaze, :string
1822
- attribute :color, :string
2323
+ Using the definition of `JapaneseCeramic` at <<encoding-instance-setting>>.
1823
2324
 
1824
- xml do
1825
- root 'Ceramic'
1826
- namespace 'http://example.com/ceramic', 'cera'
1827
- map_element 'Type', to: :type, namespace: :inherit
1828
- map_element 'Glaze', to: :glaze
1829
- map_attribute 'color', to: :color, namespace: 'http://example.com/color', prefix: 'clr'
1830
- end
1831
- end
2325
+ This XML snippet is in UTF-8.
1832
2326
 
1833
- xml_content = <<~HERE
1834
- <cera:Ceramic
1835
- xmlns:cera="http://example.com/ceramic"
1836
- xmlns:clr="http://example.com/color"
1837
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1838
- clr:color="navy-blue"
1839
- xsi:schemaLocation="
1840
- http://example.com/ceramic http://example.com/ceramic.xsd
1841
- http://example.com/color http://example.com/color.xsd
1842
- ">
1843
- <cera:Type>Porcelain</cera:Type>
1844
- <Glaze>Clear</Glaze>
1845
- </cera:Ceramic>
1846
- HERE
2327
+ [source,xml]
2328
+ ----
2329
+ <JapaneseCeramic>
2330
+ <glazeType>志野釉</glazeType>
2331
+ <description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description>
2332
+ </JapaneseCeramic>
1847
2333
  ----
1848
2334
 
1849
2335
  [source,ruby]
1850
2336
  ----
1851
- > c = Ceramic.from_xml(xml_content)
1852
- =>
1853
- #<Ceramic:0x00000001222bdd60
1854
- ...
1855
- > schema_loc = c.schema_location
1856
- #<Lutaml::Model::SchemaLocation:0x0000000122773760
1857
- ...
1858
- > schema_loc
1859
- =>
1860
- #<Lutaml::Model::SchemaLocation:0x0000000122773760
1861
- @namespace="http://www.w3.org/2001/XMLSchema-instance",
1862
- @original_schema_location="http://example.com/ceramic http://example.com/ceramic.xsd http://example.com/color http://example.com/color.xsd",
1863
- @prefix="xsi",
1864
- @schema_location=
1865
- [#<Lutaml::Model::Location:0x00000001222bd018 @location="http://example.com/ceramic.xsd", @namespace="http://example.com/ceramic">,
1866
- #<Lutaml::Model::Location:0x00000001222bcfc8 @location="http://example.com/color.xsd", @namespace="http://example.com/color">]>
1867
- > new_c = Ceramic.new(type: "Porcelain", glaze: "Clear", color: "navy-blue", schema_location: schema_loc).to_xml
1868
- > puts new_c
1869
- # <cera:Ceramic
1870
- # xmlns:cera="http://example.com/ceramic"
1871
- # xmlns:clr="http://example.com/color"
1872
- # xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1873
- # clr:color="navy-blue"
1874
- # xsi:schemaLocation="
1875
- # http://example.com/ceramic http://example.com/ceramic.xsd
1876
- # http://example.com/color http://example.com/color.xsd
1877
- # ">
1878
- # <cera:Type>Porcelain</cera:Type>
1879
- # <cera:Glaze>Clear</cera:Glaze>
1880
- # </cera:Ceramic>
2337
+ > JapaneseCeramic.from_xml(xml, encoding: "Shift_JIS")
2338
+ #=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="�菑���p���P", @description="�東京国立博物館コレクションの篠茶碗�橋本�桃山時代�">
1881
2339
  ----
1882
2340
  ====
1883
2341
 
1884
- NOTE: For details on `xsi:schemaLocation`, please refer to the
1885
- https://www.w3.org/TR/xmlschema-1/#xsi_schemaLocation[W3C XML standard].
1886
-
1887
-
1888
2342
 
1889
2343
  === Key value data models
1890
2344
 
@@ -3159,6 +3613,91 @@ end
3159
3613
  ====
3160
3614
 
3161
3615
 
3616
+ === Rendering default values (forced rendering of default values)
3617
+
3618
+ By default, attributes with default values are not rendered if the current value
3619
+ is the same as the default value.
3620
+
3621
+ In certain cases, it is necessary to render the default value even if the
3622
+ current value is the same as the default value. This is achieved by setting the
3623
+ `render_default` option to `true`.
3624
+
3625
+ Syntax:
3626
+
3627
+ [source,ruby]
3628
+ ----
3629
+ attribute :name_of_attribute, Type, default: -> { value }
3630
+
3631
+ xml do
3632
+ map_element 'name_of_attribute', to: :name_of_attribute, render_default: true
3633
+ map_attribute 'name_of_attribute', to: :name_of_attribute, render_default: true
3634
+ end
3635
+
3636
+ json | yaml | toml | key_value do
3637
+ map 'name_of_attribute', to: :name_of_attribute, render_default: true
3638
+ end
3639
+ ----
3640
+
3641
+ .Using the `render_default` option to force encoding the default value
3642
+ [example]
3643
+ ====
3644
+ [source,ruby]
3645
+ ----
3646
+ class Glaze < Lutaml::Model::Serializable
3647
+ attribute :color, :string, default: -> { 'Clear' }
3648
+ attribute :opacity, :string, default: -> { 'Opaque' }
3649
+ attribute :temperature, :integer, default: -> { 1050 }
3650
+ attribute :firing_time, :integer, default: -> { 60 }
3651
+
3652
+ xml do
3653
+ root "glaze"
3654
+ map_element 'color', to: :color
3655
+ map_element 'opacity', to: :opacity, render_default: true
3656
+ map_attribute 'temperature', to: :temperature
3657
+ map_attribute 'firingTime', to: :firing_time, render_default: true
3658
+ end
3659
+
3660
+ json do
3661
+ map 'color', to: :color
3662
+ map 'opacity', to: :opacity, render_default: true
3663
+ map 'temperature', to: :temperature
3664
+ map 'firingTime', to: :firing_time, render_default: true
3665
+ end
3666
+ end
3667
+ ----
3668
+ ====
3669
+
3670
+ .Attributes with `render_default: true` are rendered when the value is identical to the default
3671
+ [example]
3672
+ ====
3673
+ [source,ruby]
3674
+ ----
3675
+ > glaze_new = Glaze.new
3676
+ > puts glaze_new.to_xml
3677
+ # <glaze firingTime="60">
3678
+ # <opacity>Opaque</opacity>
3679
+ # </glaze>
3680
+ > puts glaze_new.to_json
3681
+ # {"firingTime":60,"opacity":"Opaque"}
3682
+ ----
3683
+ ====
3684
+
3685
+ .Attributes with `render_default: true` with non-default values are rendered
3686
+ [example]
3687
+ ====
3688
+ [source,ruby]
3689
+ ----
3690
+ > glaze = Glaze.new(color: 'Celadon', opacity: 'Semitransparent', temperature: 1300, firing_time: 90)
3691
+ > puts glaze.to_xml
3692
+ # <glaze color="Celadon" temperature="1300" firingTime="90">
3693
+ # <opacity>Semitransparent</opacity>
3694
+ # </glaze>
3695
+ > puts glaze.to_json
3696
+ # {"color":"Celadon","temperature":1300,"firingTime":90,"opacity":"Semitransparent"}
3697
+ ----
3698
+ ====
3699
+
3700
+
3162
3701
 
3163
3702
  === Advanced attribute mapping
3164
3703
 
@@ -3338,6 +3877,63 @@ end
3338
3877
  NOTE: The corresponding keyword used by Shale is `receiver:` instead of
3339
3878
  `delegate:`.
3340
3879
 
3880
+ === Value Transformations
3881
+
3882
+ The `transform` option allows defining import/export transformations at both attribute and mapping levels:
3883
+
3884
+ [source,ruby]
3885
+ ----
3886
+ class Person < Lutaml::Model::Serializable
3887
+ # Attribute-level transformation
3888
+ attribute :name, :string, transform: {
3889
+ export: ->(value) { value.upcase },
3890
+ import: ->(value) { value.downcase }
3891
+ }
3892
+
3893
+ # Mapping-level transformation in JSON format
3894
+ json do
3895
+ map "fullName", to: :name, transform: {
3896
+ export: ->(value) { "Dr. #{value}" },
3897
+ import: ->(value) { value.gsub("Dr. ", "") }
3898
+ }
3899
+ end
3900
+
3901
+ # Mapping-level transformation in XML format
3902
+ xml do
3903
+ map "full-name", to: :name, transform: {
3904
+ export: ->(value) { "Dr. #{value}" },
3905
+ import: ->(value) { value.gsub("Dr. ", "") }
3906
+ }
3907
+ end
3908
+ end
3909
+ ----
3910
+
3911
+ The transformation precedence is:
3912
+
3913
+ 1. Mapping-level transformation if defined
3914
+ 2. Attribute-level transformation if no mapping transformation exists
3915
+
3916
+ This allows flexible value transformations without needing format-specific custom methods:
3917
+
3918
+ [source,ruby]
3919
+ ----
3920
+ person = Person.new(name: "john")
3921
+
3922
+ # Uses mapping transformation
3923
+ person.to_json # => {"fullName": "Dr. john"}
3924
+
3925
+ Person.from_json({ "fullName" => "Dr. john"}.to_json).name # => john
3926
+
3927
+ # Uses attribute transformation when no mapping exists
3928
+ person.to_yaml # => name: "JOHN"
3929
+ ----
3930
+
3931
+ The `transform` option supports:
3932
+
3933
+ * `export`: Transform value during serialization
3934
+ * `import`: Transform value during deserialization
3935
+ * Collections with array transformations
3936
+ * Chaining of attribute and mapping transformations
3341
3937
 
3342
3938
  ==== Attribute serialization with custom methods
3343
3939
 
@@ -3377,15 +3973,22 @@ The following class will parse the XML snippet below:
3377
3973
 
3378
3974
  [source,ruby]
3379
3975
  ----
3976
+ class Metadata < Lutaml::Model::Serializable
3977
+ attribute :category, :string
3978
+ attribute :identifier, :string
3979
+ end
3980
+
3380
3981
  class CustomCeramic < Lutaml::Model::Serializable
3381
3982
  attribute :name, :string
3382
3983
  attribute :size, :integer
3383
3984
  attribute :description, :string
3985
+ attribute :metadata, Metadata
3384
3986
 
3385
3987
  xml do
3386
3988
  map_element "Name", to: :name, with: { to: :name_to_xml, from: :name_from_xml }
3387
3989
  map_attribute "Size", to: :size, with: { to: :size_to_xml, from: :size_from_xml }
3388
3990
  map_content with: { to: :description_to_xml, from: :description_from_xml }
3991
+ map_element :metadata, to: :metadata, with: { to: :metadata_to_xml, from: :metadata_from_xml }
3389
3992
  end
3390
3993
 
3391
3994
  def name_to_xml(model, parent, doc)
@@ -3413,6 +4016,26 @@ class CustomCeramic < Lutaml::Model::Serializable
3413
4016
  def description_from_xml(model, value)
3414
4017
  model.description = value.join.strip.sub(/^XML Description: /, "")
3415
4018
  end
4019
+
4020
+ def metadata_to_xml(model, parent, doc)
4021
+ metadata_el = doc.create_element("metadata")
4022
+ category_el = doc.create_element("category")
4023
+ identifier_el = doc.create_element("identifier")
4024
+
4025
+ doc.add_text(category_el, model.metadata.category)
4026
+ doc.add_text(identifier_el, model.metadata.identifier)
4027
+
4028
+ doc.add_element(metadata_el, category_el)
4029
+ doc.add_element(metadata_el, identifier_el)
4030
+ doc.add_element(parent, metadata_el)
4031
+ end
4032
+
4033
+ def metadata_from_xml(model, value)
4034
+ model.metadata ||= Metadata.new
4035
+
4036
+ model.metadata.category = value["elements"]["category"].text
4037
+ model.metadata.identifier = value["elements"]["identifier"].text
4038
+ end
3416
4039
  end
3417
4040
  ----
3418
4041
 
@@ -3421,6 +4044,10 @@ end
3421
4044
  <CustomCeramic Size="15">
3422
4045
  <Name>XML Masterpiece: Vase</Name>
3423
4046
  XML Description: A beautiful ceramic vase
4047
+ <metadata>
4048
+ <category>Metadata</category>
4049
+ <identifier>123</identifier>
4050
+ </metadata>
3424
4051
  </CustomCeramic>
3425
4052
  ----
3426
4053
 
@@ -3432,13 +4059,180 @@ end
3432
4059
  @name="Masterpiece: Vase",
3433
4060
  @ordered=nil,
3434
4061
  @size=12,
3435
- @description="A beautiful ceramic vase">
3436
- > puts CustomCeramic.new(name: "Vase", size: 12, description: "A beautiful vase").to_xml
4062
+ @description="A beautiful ceramic vase",
4063
+ @metadata=#<Metadata:0x0000000105ad52e0 @category="Metadata", @identifier="123">>
4064
+ > puts CustomCeramic.new(name: "Vase", size: 12, description: "A beautiful vase", metadata: Metadata.new(category: "Glaze", identifier: 15)).to_xml
3437
4065
  # <CustomCeramic Size="15">
3438
4066
  # <Name>XML Masterpiece: Vase</Name>
4067
+ # <metadata>
4068
+ # <category>Glaze</category>
4069
+ # <identifier>15</identifier>
4070
+ # </metadata>
3439
4071
  # XML Description: A beautiful vase
3440
4072
  # </CustomCeramic>
3441
4073
  ----
4074
+
4075
+ [source,ruby]
4076
+ ----
4077
+ def custom_method_from_xml(model, value)
4078
+ instance = value.node # Lutaml::Model::XmlAdapter::AdapterElement
4079
+ # OR
4080
+ instance = value.node.adapter_node # Adapter::Element
4081
+
4082
+ xml = instance.to_xml
4083
+ end
4084
+ ----
4085
+
4086
+ When building a model from XML in **custom methods**, if the `value` parameter is a `mapping_hash`, then it allows access to the parsed XML structure through `value.node` which can be converted to an XML string using `to_xml`.
4087
+
4088
+ NOTE: For `NokogiriAdapter`, we can also call `to_xml` on `value.node.adapter_node`.
4089
+
4090
+ [source,ruby]
4091
+ ----
4092
+ > value
4093
+ > # {"text"=>["\n ", "\n ", "\n "], "elements"=>{"category"=>{"text"=>"Metadata"}}}
4094
+ > value.to_xml
4095
+ > # undefined_method `to_xml`
4096
+
4097
+ > value.node
4098
+
4099
+ # Nokogiri Adapter Node
4100
+
4101
+ #<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656ed8
4102
+ # @attributes={},
4103
+ # @children=
4104
+ # [#<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656cd0 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">,
4105
+ # #<Lutaml::Model::XmlAdapter::NokogiriElement:0x00000001076569b0
4106
+ # @attributes={},
4107
+ # @children=
4108
+ # [#<Lutaml::Model::XmlAdapter::NokogiriElement:0x00000001076567f8 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
4109
+ # @default_namespace=nil,
4110
+ # @name="category",
4111
+ # @namespace_prefix=nil,
4112
+ # @text="Metadata">,
4113
+ # #<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656028 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">],
4114
+ # @default_namespace=nil,
4115
+ # @name="metadata",
4116
+ # @namespace_prefix=nil,
4117
+ # @text="\n Metadata\n ">
4118
+
4119
+ # Ox Adapter Node
4120
+
4121
+ #<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584f78
4122
+ # @attributes={},
4123
+ # @children=
4124
+ # [#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584e60
4125
+ # @attributes={},
4126
+ # @children=[#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584d48 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
4127
+ # @default_namespace=nil,
4128
+ # @name="category",
4129
+ # @namespace_prefix=nil,
4130
+ # @text="Metadata">],
4131
+ # @default_namespace=nil,
4132
+ # @name="metadata",
4133
+ # @namespace_prefix=nil,
4134
+ # @text=nil>
4135
+
4136
+ # Oga Adapter Node
4137
+
4138
+ # <Lutaml::Model::XmlAdapter::Oga::Element:0x0000000107314158
4139
+ # @attributes={},
4140
+ # @children=
4141
+ # [#<Lutaml::Model::XmlAdapter::Oga::Element:0x0000000107314090 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">,
4142
+ # #<Lutaml::Model::XmlAdapter::Oga::Element:0x000000010730fe78
4143
+ # @attributes={},
4144
+ # @children=[#<Lutaml::Model::XmlAdapter::Oga::Element:0x000000010730fd88 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
4145
+ # @default_namespace=nil,
4146
+ # @name="category",
4147
+ # @namespace_prefix=nil,
4148
+ # @text="Metadata">,
4149
+ # #<Lutaml::Model::XmlAdapter::Oga::Element:0x000000010730f8d8 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">],
4150
+ # @default_namespace=nil,
4151
+ # @name="metadata",
4152
+ # @namespace_prefix=nil,
4153
+ # @text="\n Metadata\n ">
4154
+
4155
+ > value.node.to_xml
4156
+ > #<metadata><category>Metadata</category></metadata>
4157
+ ----
4158
+ ====
4159
+
4160
+ ==== Separate Serialization Model With Custom Methods
4161
+
4162
+ [example]
4163
+ ====
4164
+ The following class will parse the XML snippet below:
4165
+
4166
+ [source,ruby]
4167
+ ----
4168
+ class CustomModelChild
4169
+ attr_accessor :street, :city
4170
+ end
4171
+
4172
+ class CustomModelChildMapper < Lutaml::Model::Serializable
4173
+ model CustomModelChild
4174
+
4175
+ attribute :street, Lutaml::Model::Type::String
4176
+ attribute :city, Lutaml::Model::Type::String
4177
+
4178
+ xml do
4179
+ map_element :street, to: :street
4180
+ map_element :city, to: :city
4181
+ end
4182
+ end
4183
+
4184
+ class CustomModelParentMapper < Lutaml::Model::Serializable
4185
+ attribute :first_name, Lutaml::Model::Type::String
4186
+ attribute :child_mapper, CustomModelChildMapper
4187
+
4188
+ xml do
4189
+ map_element :first_name, to: :first_name
4190
+ map_element :CustomModelChild,
4191
+ with: { to: :child_to_xml, from: :child_from_xml }
4192
+ end
4193
+
4194
+ def child_to_xml(model, parent, doc)
4195
+ child_el = doc.create_element("CustomModelChild")
4196
+ street_el = doc.create_element("street")
4197
+ city_el = doc.create_element("city")
4198
+
4199
+ doc.add_text(street_el, model.child_mapper.street)
4200
+ doc.add_text(city_el, model.child_mapper.city)
4201
+
4202
+ doc.add_element(child_el, street_el)
4203
+ doc.add_element(child_el, city_el)
4204
+ doc.add_element(parent, child_el)
4205
+ end
4206
+
4207
+ def child_from_xml(model, value)
4208
+ model.child_mapper ||= CustomModelChild.new
4209
+
4210
+ model.child_mapper.street = value["elements"]["street"].text
4211
+ model.child_mapper.city = value["elements"]["city"].text
4212
+ end
4213
+ end
4214
+ ----
4215
+
4216
+ [source,xml]
4217
+ ----
4218
+ <CustomModelParent>
4219
+ <first_name>John</first_name>
4220
+ <CustomModelChild>
4221
+ <street>Oxford Street</street>
4222
+ <city>London</city>
4223
+ </CustomModelChild>
4224
+ </CustomModelParent>
4225
+ ----
4226
+
4227
+ [source,ruby]
4228
+ ----
4229
+ > instance = CustomModelParentMapper.from_xml(xml)
4230
+ > #<CustomModelParent:0x0000000107c9ca68 @child_mapper=#<CustomModelChild:0x0000000107c95218 @city="London", @street="Oxford Street">, @first_name="John">
4231
+ > CustomModelParentMapper.to_xml(instance)
4232
+ > #<CustomModelParent><first_name>John</first_name><CustomModelChild><street>Oxford Street</street><city>London</city></CustomModelChild></CustomModelParent>
4233
+ ----
4234
+
4235
+ NOTE: For **custom models**, `to_xml` is called on the **mapper class**, not on **model instance**.
3442
4236
  ====
3443
4237
 
3444
4238
 
@@ -3598,6 +4392,16 @@ provided, a default directory named `lutaml_models_<timestamp>` is created.
3598
4392
  [example]
3599
4393
  `"path/to/directory"`
3600
4394
 
4395
+ `create_files`::: A `boolean` argument (`false` by default) to create files directly in the specified directory as defined by the `output_dir` option.
4396
+ +
4397
+ [example]
4398
+ `create_files: (true | false)`
4399
+
4400
+ `load_classes`::: A `boolean` argument (`false` by default) to load generated classes before returning them.
4401
+ +
4402
+ [example]
4403
+ `load_classes: (true | false)`
4404
+
3601
4405
  `namespace`::: The namespace of the schema. This will be added in the
3602
4406
  `Lutaml::Model::Serializable` file's `xml do` block.
3603
4407
  +
@@ -3619,6 +4423,7 @@ link:https://www.w3.org/TR/xmlschema-1/#include[XML Schema specification].
3619
4423
  [example]
3620
4424
  `"path/to/schema/directory"`
3621
4425
 
4426
+ NOTE: If both `create_files` and `load_classes` are provided, the `create_files` argument will take priority and generate files without loading them!
3622
4427
 
3623
4428
  The generated LutaML models consists of two different kind of Ruby classes
3624
4429
  depending on the XSD schema:
@@ -3652,9 +4457,12 @@ options = {
3652
4457
  output_dir: 'path/to/directory',
3653
4458
  namespace: 'http://example.com/namespace',
3654
4459
  prefix: "example-prefix",
3655
- location: "http://example.com/example.xsd"
4460
+ location: "http://example.com/example.xsd",
3656
4461
  # or
3657
4462
  # location: "path/to/schema/directory"
4463
+ create_files: true, # Default: false
4464
+ # OR
4465
+ load_classes: true, # Default: false
3658
4466
  }
3659
4467
 
3660
4468
  # generates the files in the output_dir | default_dir
@@ -3695,6 +4503,7 @@ Lutaml::Model supports the following validation methods:
3695
4503
 
3696
4504
  * `collection`:: Validates collection size range.
3697
4505
  * `values`:: Validates the value of an attribute from a set of fixed values.
4506
+ * `choice` :: Validates that attribute specified within defined range
3698
4507
 
3699
4508
  [example]
3700
4509
  ====
@@ -3708,6 +4517,17 @@ class Klin < Lutaml::Model::Serializable
3708
4517
  attribute :name, :string
3709
4518
  attribute :degree_settings, :integer, collection: (1..)
3710
4519
  attribute :description, :string, values: %w[one two three]
4520
+ attribute :id, :integer
4521
+ attribute :age, :integer
4522
+
4523
+ choice(min: 1, max: 1) do
4524
+ choice(min: 1, max: 2) do
4525
+ attribute :prefix, :string
4526
+ attribute :forename, :string
4527
+ end
4528
+
4529
+ attribute :nick_name, :string
4530
+ end
3711
4531
 
3712
4532
  xml do
3713
4533
  map_element 'name', to: :name
@@ -3715,26 +4535,30 @@ class Klin < Lutaml::Model::Serializable
3715
4535
  end
3716
4536
  end
3717
4537
 
3718
- klin = Klin.new(name: "Klin", degree_settings: [100, 200, 300], description: "one")
4538
+ klin = Klin.new(name: "Klin", degree_settings: [100, 200, 300], description: "one", prefix: "Ben")
3719
4539
  klin.validate
3720
4540
  # => []
3721
4541
 
3722
- klin = Klin.new(name: "Klin", degree_settings: [], description: "four")
4542
+ klin = Klin.new(name: "Klin", degree_settings: [], description: "four", prefix: "Ben", nick_name: "Smith")
3723
4543
  klin.validate
3724
4544
  # => [
3725
4545
  # #<Lutaml::Model::CollectionSizeError: degree_settings must have at least 1 element>,
3726
- # #<Lutaml::Model::ValueError: description must be one of [one, two, three]>
4546
+ # #<Lutaml::Model::ValueError: description must be one of [one, two, three]>,
4547
+ # #<Lutaml::Model::ChoiceUpperBoundError: Attribute count exceeds the upper bound>
3727
4548
  # ]
3728
4549
 
3729
4550
  e = klin.validate!
3730
4551
  # => Lutaml::Model::ValidationError: [
3731
4552
  # degree_settings must have at least 1 element,
3732
- # description must be one of [one, two, three]
4553
+ # description must be one of [one, two, three],
4554
+ # Attribute count exceeds the upper bound
3733
4555
  # ]
3734
4556
  e.errors
3735
4557
  # => [
3736
4558
  # #<Lutaml::Model::CollectionSizeError: degree_settings must have at least 1 element>,
3737
- # #<Lutaml::Model::ValueError: description must be one of [one, two, three]>
4559
+ # #<Lutaml::Model::ValueError: description must be one of [one, two, three]>,
4560
+ # #<Lutaml::Model::ChoiceUpperBoundError: Attribute count exceeds the upper bound>
4561
+ # #<Lutaml::Model::ChoiceLowerBoundError: Attribute count is less than lower bound>
3738
4562
  # ]
3739
4563
  ----
3740
4564
  ====
@@ -3771,6 +4595,30 @@ klin.validate
3771
4595
  ====
3772
4596
 
3773
4597
 
4598
+ == Liquid Compatability
4599
+
4600
+ `to_liquid` can be used to convert a class that inherit from *Lutaml::Model::Serializable* to `LiquidDrop` to be safely used in liquid templates. The returned drop provides all the attributes defined in the class as methods.
4601
+
4602
+ [example]
4603
+ ====
4604
+ [source,ruby]
4605
+ ----
4606
+ class Person < Lutaml::Model::Serializable
4607
+ attribute :name, :string
4608
+ attribute :age, integer
4609
+ end
4610
+
4611
+ person = Person.new({ name: "John", age: 22 })
4612
+ person_drop = person.to_liquid
4613
+ # Person::PersonDrop
4614
+
4615
+ puts person_drop.name
4616
+ # "John"
4617
+ puts person_drop.age
4618
+ # 22
4619
+ ----
4620
+ ====
4621
+
3774
4622
  == Adapters
3775
4623
 
3776
4624
  === General