lutaml-model 0.3.8 → 0.3.10

Sign up to get free protection for your applications and to get access to all the features.
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|