lutaml-model 0.3.8 → 0.3.10

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.
data/README.adoc CHANGED
@@ -204,11 +204,45 @@ end
204
204
  Define attributes as collections (arrays or hashes) to store multiple values
205
205
  using the `collection` option.
206
206
 
207
+ `collection` can be set to:
208
+
209
+ `true`:::
210
+ The attribute contains an unbounded collection of objects of the declared class.
211
+
212
+ `{min}..{max}`:::
213
+ The attribute contains a collection of objects of the declared class with a
214
+ count within the specified range.
215
+ If the number of objects is out of this numbered range,
216
+ `CollectionCountOutOfRangeError` will be raised.
217
+ +
218
+ [example]
219
+ ====
220
+ When set to `0..1`, it means that the attribute is optional, it could be empty
221
+ or contain one object of the declared class.
222
+ ====
223
+ +
224
+ [example]
225
+ ====
226
+ When set to `1..` (equivalent to `1..Infinity`), it means that the
227
+ attribute must contain at least one object of the declared class and can contain
228
+ any number of objects.
229
+ ====
230
+ +
231
+ [example]
232
+ ====
233
+ When set to 5..10` means that there is a minimum of 5 and a maximum of 10
234
+ objects of the declared class. If the count of values for the attribute is less
235
+ then 5 or greater then 10, the `CollectionCountOutOfRangeError` will be raised.
236
+ ====
237
+
238
+
207
239
  Syntax:
208
240
 
209
241
  [source,ruby]
210
242
  ----
211
243
  attribute :name_of_attribute, Type, collection: true
244
+ attribute :name_of_attribute, Type, collection: {min}..{max}
245
+ attribute :name_of_attribute, Type, collection: {min}..
212
246
  ----
213
247
 
214
248
  .Using the `collection` option to define a collection attribute
@@ -219,18 +253,27 @@ attribute :name_of_attribute, Type, collection: true
219
253
  class Studio < Lutaml::Model::Serializable
220
254
  attribute :location, :string
221
255
  attribute :potters, :string, collection: true
256
+ attribute :address, :string, collection: 1..2
257
+ attribute :hobbies, :string, collection: 0..
222
258
  end
223
259
  ----
224
260
 
225
261
  [source,ruby]
226
262
  ----
227
- > Studio.new.potters
263
+ > Studio.new
264
+ > # address count is `0`, must be between 1 and 2 (Lutaml::Model::CollectionCountOutOfRangeError)
265
+ > Studio.new({ address: ["address 1", "address 2", "address 3"] })
266
+ > # address count is `3`, must be between 1 and 2 (Lutaml::Model::CollectionCountOutOfRangeError)
267
+ > Studio.new({ address: ["address 1"] }).potters
228
268
  > # []
229
- > Studio.new(potters: ['John Doe', 'Jane Doe']).potters
269
+ > Studio.new({ address: ["address 1"] }).address
270
+ > # ["address 1"]
271
+ > Studio.new(address: ["address 1"], potters: ['John Doe', 'Jane Doe']).potters
230
272
  > # ['John Doe', 'Jane Doe']
231
273
  ----
232
274
  ====
233
275
 
276
+
234
277
  [[attribute-enumeration]]
235
278
  === Attribute as an enumeration
236
279
 
@@ -652,6 +695,7 @@ end
652
695
 
653
696
  ==== Namespaces
654
697
 
698
+ [[root-namespace]]
655
699
  ===== Namespace at root
656
700
 
657
701
  The `namespace` method in the `xml` block sets the namespace for the root
@@ -659,6 +703,7 @@ element.
659
703
 
660
704
  Syntax:
661
705
 
706
+ .Setting default namespace at the root element
662
707
  [source,ruby]
663
708
  ----
664
709
  xml do
@@ -666,6 +711,15 @@ xml do
666
711
  end
667
712
  ----
668
713
 
714
+ .Setting a prefixed namespace at the root element
715
+ [source,ruby]
716
+ ----
717
+ xml do
718
+ namespace 'http://example.com/namespace', 'prefix'
719
+ end
720
+ ----
721
+
722
+
669
723
  .Using the `namespace` method to set the namespace for the root element
670
724
  [example]
671
725
  ====
@@ -698,10 +752,43 @@ end
698
752
  ----
699
753
  ====
700
754
 
755
+ .Using the `namespace` method to set a prefixed namespace for the root element
756
+ [example]
757
+ ====
758
+ [source,ruby]
759
+ ----
760
+ class Ceramic < Lutaml::Model::Serializable
761
+ attribute :type, :string
762
+ attribute :glaze, :string
763
+
764
+ xml do
765
+ root 'Ceramic'
766
+ namespace 'http://example.com/ceramic', 'cer'
767
+ map_element 'Type', to: :type
768
+ map_element 'Glaze', to: :glaze
769
+ end
770
+ end
771
+ ----
772
+
773
+ [source,xml]
774
+ ----
775
+ <cer:Ceramic xmlns='http://example.com/ceramic'><cer:Type>Porcelain</cer:Type><cer:Glaze>Clear</cer:Glaze></cer:Ceramic>
776
+ ----
777
+
778
+ [source,ruby]
779
+ ----
780
+ > Ceramic.from_xml(xml_file)
781
+ > #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear">
782
+ > Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
783
+ > #<cer:Ceramic xmlns="http://example.com/ceramic"><cer:Type>Porcelain</cer:Type><cer:Glaze>Clear</cer:Glaze></cer:Ceramic>
784
+ ----
785
+ ====
786
+
787
+
701
788
  ===== Namespace on attribute
702
789
 
703
- If the namespace is defined on an XML attribute, then that will be given
704
- priority over the one defined in the class.
790
+ If the namespace is defined on a model attribute that already has a namespace,
791
+ the mapped namespace will be given priority over the one defined in the class.
705
792
 
706
793
  Syntax:
707
794
 
@@ -725,6 +812,19 @@ In this example, `glz` will be used for `Glaze` if it is added inside the
725
812
 
726
813
  [source,ruby]
727
814
  ----
815
+ class Ceramic < Lutaml::Model::Serializable
816
+ attribute :type, :string
817
+ attribute :glaze, Glaze
818
+
819
+ xml do
820
+ root 'Ceramic'
821
+ namespace 'http://example.com/ceramic'
822
+
823
+ map_element 'Type', to: :type
824
+ map_element 'Glaze', to: :glaze, namespace: 'http://example.com/glaze', prefix: "glz"
825
+ end
826
+ end
827
+
728
828
  class Glaze < Lutaml::Model::Serializable
729
829
  attribute :color, :string
730
830
  attribute :temperature, :integer
@@ -737,18 +837,6 @@ class Glaze < Lutaml::Model::Serializable
737
837
  map_element 'temperature', to: :temperature
738
838
  end
739
839
  end
740
-
741
- class Ceramic < Lutaml::Model::Serializable
742
- attribute :type, :string
743
- attribute :glaze, Glaze
744
-
745
- xml do
746
- root 'Ceramic'
747
- map_element 'Type', to: :type
748
- map_element 'Glaze', to: :glaze, namespace: 'http://example.com/glaze', prefix: "glz"
749
- map_attribute 'xmlns', to: :namespace, namespace: 'http://example.com/ceramic'
750
- end
751
- end
752
840
  ----
753
841
 
754
842
  [source,xml]
@@ -764,6 +852,11 @@ end
764
852
 
765
853
  [source,ruby]
766
854
  ----
855
+ > # Using the original Glaze class namespace
856
+ > Glaze.new(color: "Clear", temperature: 1050).to_xml
857
+ > #<glaze:Glaze xmlns="http://example.com/old_glaze"><color>Clear</color><temperature>1050</temperature></glaze:Glaze>
858
+
859
+ > # Using the Ceramic class namespace for Glaze
767
860
  > Ceramic.from_xml(xml_file)
768
861
  > #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze=#<Glaze:0x0000000104ac7240 @color="Clear", @temperature=1050>>
769
862
  > Ceramic.new(type: "Porcelain", glaze: Glaze.new(color: "Clear", temperature: 1050)).to_xml
@@ -800,7 +893,7 @@ class Ceramic < Lutaml::Model::Serializable
800
893
 
801
894
  xml do
802
895
  root 'Ceramic'
803
- namespace 'http://example.com/ceramic', prefix: 'cera'
896
+ namespace 'http://example.com/ceramic', 'cera'
804
897
  map_element 'Type', to: :type, namespace: :inherit
805
898
  map_element 'Glaze', to: :glaze
806
899
  map_attribute 'color', to: :color, namespace: 'http://example.com/color', prefix: 'clr'
@@ -810,13 +903,13 @@ end
810
903
 
811
904
  [source,xml]
812
905
  ----
813
- <Ceramic
906
+ <cera:Ceramic
814
907
  xmlns:cera='http://example.com/ceramic'
815
908
  xmlns:clr='http://example.com/color'
816
909
  clr:color="navy-blue">
817
910
  <cera:Type>Porcelain</cera:Type>
818
911
  <Glaze>Clear</Glaze>
819
- </Ceramic>
912
+ </cera:Ceramic>
820
913
  ----
821
914
 
822
915
  [source,ruby]
@@ -824,20 +917,18 @@ end
824
917
  > Ceramic.from_xml(xml_file)
825
918
  > #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear", @color="navy-blue">
826
919
  > Ceramic.new(type: "Porcelain", glaze: "Clear", color: "navy-blue").to_xml
827
- > #<Ceramic xmlns:cera="http://example.com/ceramic"
920
+ > #<cera:Ceramic xmlns:cera="http://example.com/ceramic"
828
921
  # xmlns:clr='http://example.com/color'
829
922
  # clr:color="navy-blue">
830
923
  # <cera:Type>Porcelain</cera:Type>
831
924
  # <Glaze>Clear</Glaze>
832
- # </Ceramic>
925
+ # </cera:Ceramic>
833
926
  ----
834
927
  ====
835
928
 
836
929
  [[mixed-content]]
837
930
  ==== Mixed content
838
931
 
839
- ===== General
840
-
841
932
  In XML there can be tags that contain content mixed with other tags and where
842
933
  whitespace is significant, such as to represent rich text.
843
934
 
@@ -857,9 +948,8 @@ To map this to Lutaml::Model we can use the `mixed` option in either way:
857
948
  NOTE: This feature is not supported by Shale.
858
949
 
859
950
 
860
- ===== Specifying the `mixed` option at `root`
861
-
862
- This will always treat the content of the element itself as mixed content.
951
+ To specify mixed content, the `mixed: true` option needs to be set at the
952
+ `xml` block's `root` method.
863
953
 
864
954
  Syntax:
865
955
 
@@ -876,7 +966,7 @@ end
876
966
  [source,ruby]
877
967
  ----
878
968
  class Paragraph < Lutaml::Model::Serializable
879
- attribute :bold, :string
969
+ attribute :bold, :string, collection: true # allows multiple bold tags
880
970
  attribute :italic, :string
881
971
 
882
972
  xml do
@@ -900,57 +990,125 @@ end
900
990
  TODO: How to create mixed content from `#new`?
901
991
 
902
992
 
903
- ===== Specifying the `mixed` option when referencing a model
993
+ ==== Automatic support of `xsi:schemaLocation`
904
994
 
905
- This will only treat the content of the referenced model as mixed content if the
906
- `mixed: true` is added when referencing it.
995
+ The
996
+ https://www.w3.org/TR/xmlschema-1/#xsi_schemaLocation[W3C "XMLSchema-instance"]
997
+ namespace describes a number of attributes that can be used to control the
998
+ behavior of XML processors. One of these attributes is `xsi:schemaLocation`.
907
999
 
908
- Syntax:
1000
+ The `xsi:schemaLocation` attribute locates schemas for elements and attributes
1001
+ that are in a specified namespace. Its value consists of pairs of a namespace
1002
+ URI followed by a relative or absolute URL where the schema for that namespace
1003
+ can be found.
909
1004
 
910
- [source,ruby]
1005
+ Usage of `xsi:schemaLocation` in an XML element depends on the declaration of
1006
+ the XML namespace of `xsi`, i.e.
1007
+ `xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`. Without this namespace
1008
+ LutaML will not be able to serialize the `xsi:schemaLocation` attribute.
1009
+
1010
+ NOTE: It is most commonly attached to the root element but can appear further
1011
+ down the tree.
1012
+
1013
+ The following snippet shows how `xsi:schemaLocation` is used in an XML document:
1014
+
1015
+ [source,xml]
911
1016
  ----
912
- xml do
913
- map_element 'xml_element_name', to: :name_of_attribute, mixed: true
914
- end
1017
+ <cera:Ceramic
1018
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1019
+ xmlns:cera="http://example.com/ceramic"
1020
+ xmlns:clr='http://example.com/color'
1021
+ xsi:schemaLocation=
1022
+ "http://example.com/ceramic http://example.com/ceramic.xsd
1023
+ http://example.com/color http://example.com/color.xsd"
1024
+ clr:color="navy-blue">
1025
+ <cera:Type>Porcelain</cera:Type>
1026
+ <Glaze>Clear</Glaze>
1027
+ </cera:Ceramic>
915
1028
  ----
916
1029
 
917
- .Applying `mixed` to treat an inner element as mixed content
1030
+ LutaML::Model supports the `xsi:schemaLocation` attribute in all XML
1031
+ serializations by default, through the `schema_location` attribute on the model
1032
+ instance object.
1033
+
1034
+ .Retrieving and setting the `xsi:schemaLocation` attribute in XML serialization
918
1035
  [example]
919
1036
  ====
1037
+ In this example, the `xsi:schemaLocation` attribute will be automatically
1038
+ supplied without the explicit need to define in the model, and allows for
1039
+ round-trip serialization.
1040
+
920
1041
  [source,ruby]
921
1042
  ----
922
- class Paragraph < Lutaml::Model::Serializable
923
- attribute :bold, :string
924
- attribute :italic, :string
925
-
926
- xml do
927
- root 'p'
928
-
929
- map_element 'bold', to: :bold
930
- map_element 'i', to: :italic
931
- end
932
- end
933
-
934
- class Description < Lutaml::Model::Serializable
935
- attribute :paragraph, Paragraph
1043
+ class Ceramic < Lutaml::Model::Serializable
1044
+ attribute :type, :string
1045
+ attribute :glaze, :string
1046
+ attribute :color, :string
936
1047
 
937
1048
  xml do
938
- root 'description'
939
-
940
- map_element 'p', to: :paragraph, mixed: true
1049
+ root 'Ceramic'
1050
+ namespace 'http://example.com/ceramic', 'cera'
1051
+ map_element 'Type', to: :type, namespace: :inherit
1052
+ map_element 'Glaze', to: :glaze
1053
+ map_attribute 'color', to: :color, namespace: 'http://example.com/color', prefix: 'clr'
941
1054
  end
942
1055
  end
943
- ----
944
1056
 
945
- [source,ruby]
946
- ----
947
- > Description.from_xml("<description><p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p></description>")
948
- > #<Description:0x0000000104ac7240 @paragraph=#<Paragraph:0x0000000104ac7240 @bold="John Doe", @italic="28">>
949
- > Description.new(paragraph: Paragraph.new(bold: "John Doe", italic: "28")).to_xml
950
- > #<description><p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p></description>
1057
+ xml_content = <<~HERE
1058
+ <cera:Ceramic
1059
+ xmlns:cera="http://example.com/ceramic"
1060
+ xmlns:clr="http://example.com/color"
1061
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1062
+ clr:color="navy-blue"
1063
+ xsi:schemaLocation="
1064
+ http://example.com/ceramic http://example.com/ceramic.xsd
1065
+ http://example.com/color http://example.com/color.xsd
1066
+ ">
1067
+ <cera:Type>Porcelain</cera:Type>
1068
+ <Glaze>Clear</Glaze>
1069
+ </cera:Ceramic>
1070
+ HERE
1071
+ ----
1072
+
1073
+ [source,ruby]
1074
+ ----
1075
+ > c = Ceramic.from_xml(xml_content)
1076
+ =>
1077
+ #<Ceramic:0x00000001222bdd60
1078
+ ...
1079
+ > schema_loc = c.schema_location
1080
+ #<Lutaml::Model::SchemaLocation:0x0000000122773760
1081
+ ...
1082
+ > schema_loc
1083
+ =>
1084
+ #<Lutaml::Model::SchemaLocation:0x0000000122773760
1085
+ @namespace="http://www.w3.org/2001/XMLSchema-instance",
1086
+ @original_schema_location="http://example.com/ceramic http://example.com/ceramic.xsd http://example.com/color http://example.com/color.xsd",
1087
+ @prefix="xsi",
1088
+ @schema_location=
1089
+ [#<Lutaml::Model::Location:0x00000001222bd018 @location="http://example.com/ceramic.xsd", @namespace="http://example.com/ceramic">,
1090
+ #<Lutaml::Model::Location:0x00000001222bcfc8 @location="http://example.com/color.xsd", @namespace="http://example.com/color">]>
1091
+ > new_c = Ceramic.new(type: "Porcelain", glaze: "Clear", color: "navy-blue", schema_location: schema_loc).to_xml
1092
+ > puts new_c
1093
+ # <cera:Ceramic
1094
+ # xmlns:cera="http://example.com/ceramic"
1095
+ # xmlns:clr="http://example.com/color"
1096
+ # xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1097
+ # clr:color="navy-blue"
1098
+ # xsi:schemaLocation="
1099
+ # http://example.com/ceramic http://example.com/ceramic.xsd
1100
+ # http://example.com/color http://example.com/color.xsd
1101
+ # ">
1102
+ # <cera:Type>Porcelain</cera:Type>
1103
+ # <cera:Glaze>Clear</cera:Glaze>
1104
+ # </cera:Ceramic>
951
1105
  ----
952
1106
  ====
953
1107
 
1108
+ NOTE: For details on `xsi:schemaLocation`, please refer to the
1109
+ https://www.w3.org/TR/xmlschema-1/#xsi_schemaLocation[W3C XML standard].
1110
+
1111
+
954
1112
 
955
1113
  === Key value data models
956
1114
 
@@ -1337,12 +1495,12 @@ class CustomCeramic < Lutaml::Model::Serializable
1337
1495
  map 'size', to: :size
1338
1496
  end
1339
1497
 
1340
- def name_to_json(model, value)
1341
- doc["name"] = "Masterpiece: #{value}"
1498
+ def name_to_json(model, doc)
1499
+ doc["name"] = "Masterpiece: #{model.name}"
1342
1500
  end
1343
1501
 
1344
- def name_from_json(model, doc)
1345
- model.name = value.sub(/^JSON Masterpiece: /, '')
1502
+ def name_from_json(model, value)
1503
+ model.name = value.sub(/^Masterpiece: /, '')
1346
1504
  end
1347
1505
  end
1348
1506
  ----
@@ -1551,6 +1709,101 @@ In this example:
1551
1709
  ====
1552
1710
 
1553
1711
 
1712
+ == Validation
1713
+
1714
+ === General
1715
+
1716
+ Lutaml::Model provides a way to validate data models using the `validate` and
1717
+ `validate!` methods.
1718
+
1719
+ * The `validate` method sets an `errors` array in the model instance that
1720
+ contains all the validation errors. This method is used for checking the
1721
+ validity of the model silently.
1722
+
1723
+ * The `validate!` method raises a `Lutaml::Model::ValidationError` that contains
1724
+ all the validation errors. This method is used for forceful validation of the
1725
+ model through raising an error.
1726
+
1727
+ Lutaml::Model supports the following validation methods:
1728
+
1729
+ * `collection`:: Validates collection size range.
1730
+ * `values`:: Validates the value of an attribute from a set of fixed values.
1731
+
1732
+ [example]
1733
+ ====
1734
+ The following class will validate the `degree_settings` attribute to ensure that
1735
+ it has at least one element and that the `description` attribute is one of the
1736
+ values in the set `[one, two, three]`.
1737
+
1738
+ [source,ruby]
1739
+ ----
1740
+ class Klin < Lutaml::Model::Serializable
1741
+ attribute :name, :string
1742
+ attribute :degree_settings, :integer, collection: (1..)
1743
+ attribute :description, :string, values: %w[one two three]
1744
+
1745
+ xml do
1746
+ map_element 'name', to: :name
1747
+ map_attribute 'degree_settings', to: :degree_settings
1748
+ end
1749
+ end
1750
+
1751
+ klin = Klin.new(name: "Klin", degree_settings: [100, 200, 300], description: "one")
1752
+ klin.validate
1753
+ # => []
1754
+
1755
+ klin = Klin.new(name: "Klin", degree_settings: [], description: "four")
1756
+ klin.validate
1757
+ # => [
1758
+ # #<Lutaml::Model::CollectionSizeError: degree_settings must have at least 1 element>,
1759
+ # #<Lutaml::Model::ValueError: description must be one of [one, two, three]>
1760
+ # ]
1761
+
1762
+ e = klin.validate!
1763
+ # => Lutaml::Model::ValidationError: [
1764
+ # degree_settings must have at least 1 element,
1765
+ # description must be one of [one, two, three]
1766
+ # ]
1767
+ e.errors
1768
+ # => [
1769
+ # #<Lutaml::Model::CollectionSizeError: degree_settings must have at least 1 element>,
1770
+ # #<Lutaml::Model::ValueError: description must be one of [one, two, three]>
1771
+ # ]
1772
+ ----
1773
+ ====
1774
+
1775
+ === Custom validation
1776
+
1777
+ To add custom validation, override the `validate` method in the model class.
1778
+ Additional errors should be added to the `errors` array.
1779
+
1780
+ [example]
1781
+ ====
1782
+ The following class validates the `degree_settings` attribute when the `type` is
1783
+ `glass` to ensure that the value is less than 1300.
1784
+
1785
+ [source,ruby]
1786
+ ----
1787
+ class Klin < Lutaml::Model::Serializable
1788
+ attribute :name, :string
1789
+ attribute :type, :string, values: %w[glass ceramic]
1790
+ attribute :degree_settings, :integer, collection: (1..)
1791
+
1792
+ def validate
1793
+ errors = super
1794
+ if type == "glass" && degree_settings.any? { |d| d > 1300 }
1795
+ errors << Lutaml::Model::Error.new("Degree settings for glass must be less than 1300")
1796
+ end
1797
+ end
1798
+ end
1799
+
1800
+ klin = Klin.new(name: "Klin", type: "glass", degree_settings: [100, 200, 1400])
1801
+ klin.validate
1802
+ # => [#<Lutaml::Model::Error: Degree settings for glass must be less than 1300>]
1803
+ ----
1804
+ ====
1805
+
1806
+
1554
1807
  == Adapters
1555
1808
 
1556
1809
  === General
@@ -1579,7 +1832,7 @@ Lutaml::Model::Config.configure do |config|
1579
1832
  end
1580
1833
  ----
1581
1834
 
1582
- You can also provide the adapter type by using symbols like
1835
+ You can also provide the adapter type by using symbols like
1583
1836
 
1584
1837
  [source,ruby]
1585
1838
  ----
@@ -1765,6 +2018,11 @@ differences in implementation.
1765
2018
 
1766
2019
  4+h| XML features
1767
2020
 
2021
+ | <<root-namespace,XML default namespace>>
2022
+ | Yes. Supports `<root xmlns='http://example.com'>` through the `namespace` option without prefix.
2023
+ | No. Only supports `<root xmlns:prefix='http://example.com'>`.
2024
+ |
2025
+
1768
2026
  | XML mixed content support
1769
2027
  | Yes. Supports the following kind of XML through <<mixed-content,mixed content>> support.
1770
2028
 
@@ -6,11 +6,11 @@ module Lutaml
6
6
  def initialize(name, type, options = {})
7
7
  @name = name
8
8
  @type = cast_type(type)
9
-
10
9
  @options = options
11
10
 
12
- if collection? && !options[:default]
13
- @options[:default] = -> { [] }
11
+ if collection?
12
+ validate_collection_range
13
+ @options[:default] = -> { [] } unless options[:default]
14
14
  end
15
15
  end
16
16
 
@@ -31,6 +31,10 @@ module Lutaml
31
31
  options[:collection] || false
32
32
  end
33
33
 
34
+ def singular?
35
+ !collection?
36
+ end
37
+
34
38
  def default
35
39
  return options[:default].call if options[:default].is_a?(Proc)
36
40
 
@@ -41,6 +45,113 @@ module Lutaml
41
45
  options.fetch(:render_nil, false)
42
46
  end
43
47
 
48
+ def enum_values
49
+ @options.key?(:values) ? @options[:values] : []
50
+ end
51
+
52
+ # Check if the value to be assigned is valid for the attribute
53
+ #
54
+ # Currently there are 2 validations
55
+ # 1. Value should be from the values list if they are defined
56
+ # e.g values: ["foo", "bar"] is set then any other value for this
57
+ # attribute will raise `Lutaml::Model::InvalidValueError`
58
+ #
59
+ # 2. Value count should be between the collection range if defined
60
+ # e.g if collection: 0..5 is set then the value greater then 5
61
+ # will raise `Lutaml::Model::CollectionCountOutOfRangeError`
62
+ def validate_value!(value)
63
+ valid_value!(value)
64
+ valid_collection!(value)
65
+ end
66
+
67
+ def valid_value!(value)
68
+ return true if value.nil? && !collection?
69
+ return true if enum_values.empty?
70
+
71
+ unless valid_value?(value)
72
+ raise Lutaml::Model::InvalidValueError.new(name, value, enum_values)
73
+ end
74
+
75
+ true
76
+ end
77
+
78
+ def valid_value?(value)
79
+ return true unless options[:values]
80
+
81
+ options[:values].include?(value)
82
+ end
83
+
84
+ def validate_value!(value)
85
+ # return true if none of the validations are present
86
+ return true if enum_values.empty? && singular?
87
+
88
+ # Use the default value if the value is nil
89
+ value = default if value.nil?
90
+
91
+ valid_value!(value) && valid_collection!(value)
92
+ end
93
+
94
+ def validate_collection_range
95
+ range = @options[:collection]
96
+ return if range == true
97
+
98
+ unless range.is_a?(Range)
99
+ raise ArgumentError, "Invalid collection range: #{range}"
100
+ end
101
+
102
+ if range.begin.nil?
103
+ raise ArgumentError,
104
+ "Invalid collection range: #{range}. Begin must be specified."
105
+ end
106
+
107
+ if range.begin.negative?
108
+ raise ArgumentError,
109
+ "Invalid collection range: #{range}. Begin must be non-negative."
110
+ end
111
+
112
+ if range.end && range.end < range.begin
113
+ raise ArgumentError,
114
+ "Invalid collection range: #{range}. End must be greater than or equal to begin."
115
+ end
116
+ end
117
+
118
+ def valid_collection!(value)
119
+ return true unless collection?
120
+
121
+ # Allow nil values for collections during initialization
122
+ return true if value.nil?
123
+
124
+ # Allow any value for unbounded collections
125
+ return true if options[:collection] == true
126
+
127
+ unless value.is_a?(Array)
128
+ raise Lutaml::Model::CollectionCountOutOfRangeError.new(
129
+ name,
130
+ value,
131
+ options[:collection],
132
+ )
133
+ end
134
+
135
+ range = options[:collection]
136
+ return true unless range.is_a?(Range)
137
+
138
+ if range.end.nil?
139
+ if value.size < range.begin
140
+ raise Lutaml::Model::CollectionCountOutOfRangeError.new(
141
+ name,
142
+ value,
143
+ range,
144
+ )
145
+ end
146
+ elsif !range.cover?(value.size)
147
+ raise Lutaml::Model::CollectionCountOutOfRangeError.new(
148
+ name,
149
+ value,
150
+ range,
151
+ )
152
+ end
153
+ end
154
+
44
155
  def serialize(value, format, options = {})
45
156
  if value.is_a?(Array)
46
157
  value.map do |v|