lutaml-model 0.3.9 → 0.3.11

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
@@ -145,6 +145,8 @@ same class and all their attributes are equal.
145
145
 
146
146
  === Supported attribute value types
147
147
 
148
+ ==== General types
149
+
148
150
  Lutaml::Model supports the following attribute types, they can be
149
151
  referred by a string, a symbol, or their class constant.
150
152
 
@@ -166,12 +168,11 @@ attribute :name_of_attribute, {symbol | string | class}
166
168
  | `DateTime` | `:date_time` | `Lutaml::Model::Type::DateTime` | `::DateTime`
167
169
  | `TimeWithoutDate` | `:time_without_date` | `Lutaml::Model::Type::TimeWithoutDate` | `::Time`
168
170
  | `Boolean` | `:boolean` | `Lutaml::Model::Type::Boolean` | `Boolean`
169
- | `Decimal` | `:decimal` | `Lutaml::Model::Type::Decimal` | `::BigDecimal`
171
+ | `Decimal` (optional) | `:decimal` | `Lutaml::Model::Type::Decimal` | `::BigDecimal`
170
172
  | `Hash` | `:hash` | `Lutaml::Model::Type::Hash` | `::Hash`
171
173
 
172
174
  |===
173
175
 
174
-
175
176
  .Defining attributes with supported types via symbol, string and class
176
177
  [example]
177
178
  ====
@@ -198,17 +199,68 @@ end
198
199
  ----
199
200
  ====
200
201
 
202
+ ==== (optional) Decimal type
203
+
204
+ The `BigDecimal` class is no longer part of the standard Ruby library from Ruby
205
+ 3.4 onwards, hence the `Decimal` type is only enabled when the `bigdecimal`
206
+ library is loaded.
207
+
208
+ This means that the following code needs to be run before using (and parsing)
209
+ the `Decimal` type:
210
+
211
+ [source,ruby]
212
+ ----
213
+ require 'bigdecimal'
214
+ ----
215
+
216
+ If the `bigdecimal` library is not loaded, usage of the `Decimal` type will
217
+ raise a `Lutaml::Model::TypeNotSupportedError`.
218
+
201
219
 
202
220
  === Attribute as a collection
203
221
 
204
222
  Define attributes as collections (arrays or hashes) to store multiple values
205
223
  using the `collection` option.
206
224
 
225
+ `collection` can be set to:
226
+
227
+ `true`:::
228
+ The attribute contains an unbounded collection of objects of the declared class.
229
+
230
+ `{min}..{max}`:::
231
+ The attribute contains a collection of objects of the declared class with a
232
+ count within the specified range.
233
+ If the number of objects is out of this numbered range,
234
+ `CollectionCountOutOfRangeError` will be raised.
235
+ +
236
+ [example]
237
+ ====
238
+ When set to `0..1`, it means that the attribute is optional, it could be empty
239
+ or contain one object of the declared class.
240
+ ====
241
+ +
242
+ [example]
243
+ ====
244
+ When set to `1..` (equivalent to `1..Infinity`), it means that the
245
+ attribute must contain at least one object of the declared class and can contain
246
+ any number of objects.
247
+ ====
248
+ +
249
+ [example]
250
+ ====
251
+ When set to 5..10` means that there is a minimum of 5 and a maximum of 10
252
+ objects of the declared class. If the count of values for the attribute is less
253
+ then 5 or greater then 10, the `CollectionCountOutOfRangeError` will be raised.
254
+ ====
255
+
256
+
207
257
  Syntax:
208
258
 
209
259
  [source,ruby]
210
260
  ----
211
261
  attribute :name_of_attribute, Type, collection: true
262
+ attribute :name_of_attribute, Type, collection: {min}..{max}
263
+ attribute :name_of_attribute, Type, collection: {min}..
212
264
  ----
213
265
 
214
266
  .Using the `collection` option to define a collection attribute
@@ -219,18 +271,27 @@ attribute :name_of_attribute, Type, collection: true
219
271
  class Studio < Lutaml::Model::Serializable
220
272
  attribute :location, :string
221
273
  attribute :potters, :string, collection: true
274
+ attribute :address, :string, collection: 1..2
275
+ attribute :hobbies, :string, collection: 0..
222
276
  end
223
277
  ----
224
278
 
225
279
  [source,ruby]
226
280
  ----
227
- > Studio.new.potters
281
+ > Studio.new
282
+ > # address count is `0`, must be between 1 and 2 (Lutaml::Model::CollectionCountOutOfRangeError)
283
+ > Studio.new({ address: ["address 1", "address 2", "address 3"] })
284
+ > # address count is `3`, must be between 1 and 2 (Lutaml::Model::CollectionCountOutOfRangeError)
285
+ > Studio.new({ address: ["address 1"] }).potters
228
286
  > # []
229
- > Studio.new(potters: ['John Doe', 'Jane Doe']).potters
287
+ > Studio.new({ address: ["address 1"] }).address
288
+ > # ["address 1"]
289
+ > Studio.new(address: ["address 1"], potters: ['John Doe', 'Jane Doe']).potters
230
290
  > # ['John Doe', 'Jane Doe']
231
291
  ----
232
292
  ====
233
293
 
294
+
234
295
  [[attribute-enumeration]]
235
296
  === Attribute as an enumeration
236
297
 
@@ -946,6 +1007,125 @@ end
946
1007
 
947
1008
  TODO: How to create mixed content from `#new`?
948
1009
 
1010
+ [[xml-schema-location]]
1011
+ ==== Automatic support of `xsi:schemaLocation`
1012
+
1013
+ The
1014
+ https://www.w3.org/TR/xmlschema-1/#xsi_schemaLocation[W3C "XMLSchema-instance"]
1015
+ namespace describes a number of attributes that can be used to control the
1016
+ behavior of XML processors. One of these attributes is `xsi:schemaLocation`.
1017
+
1018
+ The `xsi:schemaLocation` attribute locates schemas for elements and attributes
1019
+ that are in a specified namespace. Its value consists of pairs of a namespace
1020
+ URI followed by a relative or absolute URL where the schema for that namespace
1021
+ can be found.
1022
+
1023
+ Usage of `xsi:schemaLocation` in an XML element depends on the declaration of
1024
+ the XML namespace of `xsi`, i.e.
1025
+ `xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`. Without this namespace
1026
+ LutaML will not be able to serialize the `xsi:schemaLocation` attribute.
1027
+
1028
+ NOTE: It is most commonly attached to the root element but can appear further
1029
+ down the tree.
1030
+
1031
+ The following snippet shows how `xsi:schemaLocation` is used in an XML document:
1032
+
1033
+ [source,xml]
1034
+ ----
1035
+ <cera:Ceramic
1036
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1037
+ xmlns:cera="http://example.com/ceramic"
1038
+ xmlns:clr='http://example.com/color'
1039
+ xsi:schemaLocation=
1040
+ "http://example.com/ceramic http://example.com/ceramic.xsd
1041
+ http://example.com/color http://example.com/color.xsd"
1042
+ clr:color="navy-blue">
1043
+ <cera:Type>Porcelain</cera:Type>
1044
+ <Glaze>Clear</Glaze>
1045
+ </cera:Ceramic>
1046
+ ----
1047
+
1048
+ LutaML::Model supports the `xsi:schemaLocation` attribute in all XML
1049
+ serializations by default, through the `schema_location` attribute on the model
1050
+ instance object.
1051
+
1052
+ .Retrieving and setting the `xsi:schemaLocation` attribute in XML serialization
1053
+ [example]
1054
+ ====
1055
+ In this example, the `xsi:schemaLocation` attribute will be automatically
1056
+ supplied without the explicit need to define in the model, and allows for
1057
+ round-trip serialization.
1058
+
1059
+ [source,ruby]
1060
+ ----
1061
+ class Ceramic < Lutaml::Model::Serializable
1062
+ attribute :type, :string
1063
+ attribute :glaze, :string
1064
+ attribute :color, :string
1065
+
1066
+ xml do
1067
+ root 'Ceramic'
1068
+ namespace 'http://example.com/ceramic', 'cera'
1069
+ map_element 'Type', to: :type, namespace: :inherit
1070
+ map_element 'Glaze', to: :glaze
1071
+ map_attribute 'color', to: :color, namespace: 'http://example.com/color', prefix: 'clr'
1072
+ end
1073
+ end
1074
+
1075
+ xml_content = <<~HERE
1076
+ <cera:Ceramic
1077
+ xmlns:cera="http://example.com/ceramic"
1078
+ xmlns:clr="http://example.com/color"
1079
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1080
+ clr:color="navy-blue"
1081
+ xsi:schemaLocation="
1082
+ http://example.com/ceramic http://example.com/ceramic.xsd
1083
+ http://example.com/color http://example.com/color.xsd
1084
+ ">
1085
+ <cera:Type>Porcelain</cera:Type>
1086
+ <Glaze>Clear</Glaze>
1087
+ </cera:Ceramic>
1088
+ HERE
1089
+ ----
1090
+
1091
+ [source,ruby]
1092
+ ----
1093
+ > c = Ceramic.from_xml(xml_content)
1094
+ =>
1095
+ #<Ceramic:0x00000001222bdd60
1096
+ ...
1097
+ > schema_loc = c.schema_location
1098
+ #<Lutaml::Model::SchemaLocation:0x0000000122773760
1099
+ ...
1100
+ > schema_loc
1101
+ =>
1102
+ #<Lutaml::Model::SchemaLocation:0x0000000122773760
1103
+ @namespace="http://www.w3.org/2001/XMLSchema-instance",
1104
+ @original_schema_location="http://example.com/ceramic http://example.com/ceramic.xsd http://example.com/color http://example.com/color.xsd",
1105
+ @prefix="xsi",
1106
+ @schema_location=
1107
+ [#<Lutaml::Model::Location:0x00000001222bd018 @location="http://example.com/ceramic.xsd", @namespace="http://example.com/ceramic">,
1108
+ #<Lutaml::Model::Location:0x00000001222bcfc8 @location="http://example.com/color.xsd", @namespace="http://example.com/color">]>
1109
+ > new_c = Ceramic.new(type: "Porcelain", glaze: "Clear", color: "navy-blue", schema_location: schema_loc).to_xml
1110
+ > puts new_c
1111
+ # <cera:Ceramic
1112
+ # xmlns:cera="http://example.com/ceramic"
1113
+ # xmlns:clr="http://example.com/color"
1114
+ # xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1115
+ # clr:color="navy-blue"
1116
+ # xsi:schemaLocation="
1117
+ # http://example.com/ceramic http://example.com/ceramic.xsd
1118
+ # http://example.com/color http://example.com/color.xsd
1119
+ # ">
1120
+ # <cera:Type>Porcelain</cera:Type>
1121
+ # <cera:Glaze>Clear</cera:Glaze>
1122
+ # </cera:Ceramic>
1123
+ ----
1124
+ ====
1125
+
1126
+ NOTE: For details on `xsi:schemaLocation`, please refer to the
1127
+ https://www.w3.org/TR/xmlschema-1/#xsi_schemaLocation[W3C XML standard].
1128
+
949
1129
 
950
1130
 
951
1131
  === Key value data models
@@ -1282,9 +1462,14 @@ NOTE: The corresponding keyword used by Shale is `receiver:` instead of
1282
1462
 
1283
1463
  ==== Attribute serialization with custom methods
1284
1464
 
1465
+ ===== General
1466
+
1285
1467
  Define custom methods for specific attribute mappings using the `with:` key for
1286
1468
  each serialization mapping block for `from` and `to`.
1287
1469
 
1470
+
1471
+ ===== XML serialization with custom methods
1472
+
1288
1473
  Syntax:
1289
1474
 
1290
1475
  .XML serialization with custom methods
@@ -1306,6 +1491,81 @@ xml do
1306
1491
  end
1307
1492
  ----
1308
1493
 
1494
+ .Using the `with:` key to define custom serialization methods for XML
1495
+ [example]
1496
+ ====
1497
+ The following class will parse the XML snippet below:
1498
+
1499
+ [source,ruby]
1500
+ ----
1501
+ class CustomCeramic < Lutaml::Model::Serializable
1502
+ attribute :name, :string
1503
+ attribute :size, :integer
1504
+ attribute :description, :string
1505
+
1506
+ xml do
1507
+ map_element "Name", to: :name, with: { to: :name_to_xml, from: :name_from_xml }
1508
+ map_attribute "Size", to: :size, with: { to: :size_to_xml, from: :size_from_xml }
1509
+ map_content with: { to: :description_to_xml, from: :description_from_xml }
1510
+ end
1511
+
1512
+ def name_to_xml(model, parent, doc)
1513
+ el = doc.create_element("Name")
1514
+ doc.add_text(el, "XML Masterpiece: #{model.name}")
1515
+ doc.add_element(parent, el)
1516
+ end
1517
+
1518
+ def name_from_xml(model, value)
1519
+ model.name = value.sub(/^XML Masterpiece: /, "")
1520
+ end
1521
+
1522
+ def size_to_xml(model, parent, doc)
1523
+ doc.add_attribute(parent, "Size", model.size + 3)
1524
+ end
1525
+
1526
+ def size_from_xml(model, value)
1527
+ model.size = value.to_i - 3
1528
+ end
1529
+
1530
+ def description_to_xml(model, parent, doc)
1531
+ doc.add_text(parent, "XML Description: #{model.description}")
1532
+ end
1533
+
1534
+ def description_from_xml(model, value)
1535
+ model.description = value.join.strip.sub(/^XML Description: /, "")
1536
+ end
1537
+ end
1538
+ ----
1539
+
1540
+ [source,xml]
1541
+ ----
1542
+ <CustomCeramic Size="15">
1543
+ <Name>XML Masterpiece: Vase</Name>
1544
+ XML Description: A beautiful ceramic vase
1545
+ </CustomCeramic>
1546
+ ----
1547
+
1548
+ [source,ruby]
1549
+ ----
1550
+ > CustomCeramic.from_xml(xml)
1551
+ > #<CustomCeramic:0x0000000108d0e1f8
1552
+ @element_order=["text", "Name", "text", "Size", "text"],
1553
+ @name="Masterpiece: Vase",
1554
+ @ordered=nil,
1555
+ @size=12,
1556
+ @description="A beautiful ceramic vase",
1557
+ @validate_on_set=false>
1558
+ > puts CustomCeramic.new(name: "Vase", size: 12, description: "A beautiful vase").to_xml
1559
+ # <CustomCeramic Size="15">
1560
+ # <Name>XML Masterpiece: Vase</Name>
1561
+ # XML Description: A beautiful vase
1562
+ # </CustomCeramic>
1563
+ ----
1564
+ ====
1565
+
1566
+
1567
+ ===== Key-value data model serialization with custom methods
1568
+
1309
1569
  .Key-value data model serialization with custom methods
1310
1570
  [source,ruby]
1311
1571
  ----
@@ -1333,12 +1593,12 @@ class CustomCeramic < Lutaml::Model::Serializable
1333
1593
  map 'size', to: :size
1334
1594
  end
1335
1595
 
1336
- def name_to_json(model, value)
1337
- doc["name"] = "Masterpiece: #{value}"
1596
+ def name_to_json(model, doc)
1597
+ doc["name"] = "Masterpiece: #{model.name}"
1338
1598
  end
1339
1599
 
1340
- def name_from_json(model, doc)
1341
- model.name = value.sub(/^JSON Masterpiece: /, '')
1600
+ def name_from_json(model, value)
1601
+ model.name = value.sub(/^Masterpiece: /, '')
1342
1602
  end
1343
1603
  end
1344
1604
  ----
@@ -1362,7 +1622,7 @@ end
1362
1622
 
1363
1623
 
1364
1624
  [[attribute-extraction]]
1365
- ==== Attribute extraction
1625
+ ==== Attribute extraction (for key-value data models only)
1366
1626
 
1367
1627
  NOTE: This feature is for key-value data model serialization only.
1368
1628
 
@@ -1547,6 +1807,101 @@ In this example:
1547
1807
  ====
1548
1808
 
1549
1809
 
1810
+ == Validation
1811
+
1812
+ === General
1813
+
1814
+ Lutaml::Model provides a way to validate data models using the `validate` and
1815
+ `validate!` methods.
1816
+
1817
+ * The `validate` method sets an `errors` array in the model instance that
1818
+ contains all the validation errors. This method is used for checking the
1819
+ validity of the model silently.
1820
+
1821
+ * The `validate!` method raises a `Lutaml::Model::ValidationError` that contains
1822
+ all the validation errors. This method is used for forceful validation of the
1823
+ model through raising an error.
1824
+
1825
+ Lutaml::Model supports the following validation methods:
1826
+
1827
+ * `collection`:: Validates collection size range.
1828
+ * `values`:: Validates the value of an attribute from a set of fixed values.
1829
+
1830
+ [example]
1831
+ ====
1832
+ The following class will validate the `degree_settings` attribute to ensure that
1833
+ it has at least one element and that the `description` attribute is one of the
1834
+ values in the set `[one, two, three]`.
1835
+
1836
+ [source,ruby]
1837
+ ----
1838
+ class Klin < Lutaml::Model::Serializable
1839
+ attribute :name, :string
1840
+ attribute :degree_settings, :integer, collection: (1..)
1841
+ attribute :description, :string, values: %w[one two three]
1842
+
1843
+ xml do
1844
+ map_element 'name', to: :name
1845
+ map_attribute 'degree_settings', to: :degree_settings
1846
+ end
1847
+ end
1848
+
1849
+ klin = Klin.new(name: "Klin", degree_settings: [100, 200, 300], description: "one")
1850
+ klin.validate
1851
+ # => []
1852
+
1853
+ klin = Klin.new(name: "Klin", degree_settings: [], description: "four")
1854
+ klin.validate
1855
+ # => [
1856
+ # #<Lutaml::Model::CollectionSizeError: degree_settings must have at least 1 element>,
1857
+ # #<Lutaml::Model::ValueError: description must be one of [one, two, three]>
1858
+ # ]
1859
+
1860
+ e = klin.validate!
1861
+ # => Lutaml::Model::ValidationError: [
1862
+ # degree_settings must have at least 1 element,
1863
+ # description must be one of [one, two, three]
1864
+ # ]
1865
+ e.errors
1866
+ # => [
1867
+ # #<Lutaml::Model::CollectionSizeError: degree_settings must have at least 1 element>,
1868
+ # #<Lutaml::Model::ValueError: description must be one of [one, two, three]>
1869
+ # ]
1870
+ ----
1871
+ ====
1872
+
1873
+ === Custom validation
1874
+
1875
+ To add custom validation, override the `validate` method in the model class.
1876
+ Additional errors should be added to the `errors` array.
1877
+
1878
+ [example]
1879
+ ====
1880
+ The following class validates the `degree_settings` attribute when the `type` is
1881
+ `glass` to ensure that the value is less than 1300.
1882
+
1883
+ [source,ruby]
1884
+ ----
1885
+ class Klin < Lutaml::Model::Serializable
1886
+ attribute :name, :string
1887
+ attribute :type, :string, values: %w[glass ceramic]
1888
+ attribute :degree_settings, :integer, collection: (1..)
1889
+
1890
+ def validate
1891
+ errors = super
1892
+ if type == "glass" && degree_settings.any? { |d| d > 1300 }
1893
+ errors << Lutaml::Model::Error.new("Degree settings for glass must be less than 1300")
1894
+ end
1895
+ end
1896
+ end
1897
+
1898
+ klin = Klin.new(name: "Klin", type: "glass", degree_settings: [100, 200, 1400])
1899
+ klin.validate
1900
+ # => [#<Lutaml::Model::Error: Degree settings for glass must be less than 1300>]
1901
+ ----
1902
+ ====
1903
+
1904
+
1550
1905
  == Adapters
1551
1906
 
1552
1907
  === General
@@ -1735,9 +2090,9 @@ differences in implementation.
1735
2090
  |
1736
2091
 
1737
2092
  | Value types
1738
- | `Lutaml::Model::Type` includes: `Integer`, `String`, `Float`, `Boolean`, `Date`, `DateTime`, `Time`, `Hash`.
2093
+ | `Lutaml::Model::Type` includes: `Integer`, `String`, `Float`, `Boolean`, `Date`, `DateTime`, `Time`, `Decimal`, `Hash`.
1739
2094
  | `Shale::Type` includes: `Integer`, `String`, `Float`, `Boolean`, `Date`, `Time`.
1740
- | Lutaml::Model supports the additional value types `DateTime` and `Hash`.
2095
+ | Lutaml::Model supports additional value types `Decimal`, `DateTime` and `Hash`.
1741
2096
 
1742
2097
  | Configuration
1743
2098
  | `Lutaml::Model::Config`
@@ -1754,6 +2109,11 @@ differences in implementation.
1754
2109
  | XML, YAML, JSON, TOML, CSV
1755
2110
  | Lutaml::Model does not support CSV.
1756
2111
 
2112
+ | Validation
2113
+ | Supports collection range, fixed values, and custom validation
2114
+ | Requires implementation
2115
+ |
2116
+
1757
2117
  | Adapter support
1758
2118
  | XML (Nokogiri, Ox, Oga), YAML, JSON (JSON, MultiJson), TOML (Toml-rb, Tomlib)
1759
2119
  | XML (Nokogiri, Ox), YAML, JSON (JSON, MultiJson), TOML (Toml-rb, Tomlib), CSV
@@ -1785,6 +2145,12 @@ namespace from the root element.
1785
2145
  | No.
1786
2146
  |
1787
2147
 
2148
+ | Support for `xsi:schemaLocation`
2149
+ | Yes. Automatically supports the <<xml-schema-location,`xsi:schemaLocation`>>
2150
+ attribute for every element.
2151
+ | Requires manual specification on every XML element that uses it.
2152
+ |
2153
+
1788
2154
  4+h| Attribute features
1789
2155
 
1790
2156
  | Attribute delegation
@@ -1804,7 +2170,6 @@ data models.
1804
2170
  | No.
1805
2171
  | Lutaml::Model supports attribute extraction from key-value data models.
1806
2172
 
1807
-
1808
2173
  |===
1809
2174
 
1810
2175
 
@@ -1857,25 +2222,26 @@ Actions:
1857
2222
 
1858
2223
  === Step 2: Replace value type definitions
1859
2224
 
1860
- Value types in `Lutaml::Model` are under the `Lutaml::Model::Type` module.
2225
+ Value types in `Lutaml::Model` are under the `Lutaml::Model::Type` module,
2226
+ or use the LutaML type symbols.
1861
2227
 
1862
2228
  [source,ruby]
1863
2229
  ----
1864
2230
  class Example < Lutaml::Model::Serializable
1865
- attribute :length, Lutaml::Model::Type::Integer
1866
- attribute :description, Lutaml::Model::Type::String
2231
+ attribute :length, :integer
2232
+ attribute :description, :string
1867
2233
  end
1868
2234
  ----
1869
2235
 
1870
2236
  [NOTE]
1871
2237
  ====
1872
- `Lutaml::Model` also supports specifying predefined value types as strings or
2238
+ `Lutaml::Model` supports specifying predefined value types as strings or
1873
2239
  symbols, which is not supported by Shale.
1874
2240
 
1875
2241
  [source,ruby]
1876
2242
  ----
1877
2243
  class Example < Lutaml::Model::Serializable
1878
- attribute :length, :integer
2244
+ attribute :length, Lutaml::Model::Type::Integer
1879
2245
  attribute :description, "String"
1880
2246
  end
1881
2247
  ----
@@ -2112,6 +2478,6 @@ allowing you to shape and structure your data into useful forms.
2112
2478
  == License and Copyright
2113
2479
 
2114
2480
  This project is licensed under the BSD 2-clause License.
2115
- See the LICENSE file for details.
2481
+ See the link:LICENSE.md[] file for details.
2116
2482
 
2117
2483
  Copyright Ribose.
@@ -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
 
@@ -27,20 +27,145 @@ module Lutaml
27
27
  raise ArgumentError, "Unknown Lutaml::Model::Type: #{type}"
28
28
  end
29
29
 
30
+ def cast_value(value)
31
+ return type.cast(value) unless value.is_a?(Array)
32
+
33
+ value.map { |v| type.cast(v) }
34
+ end
35
+
36
+ def setter
37
+ :"#{@name}="
38
+ end
39
+
30
40
  def collection?
31
41
  options[:collection] || false
32
42
  end
33
43
 
44
+ def singular?
45
+ !collection?
46
+ end
47
+
34
48
  def default
35
- return options[:default].call if options[:default].is_a?(Proc)
49
+ value = if options[:default].is_a?(Proc)
50
+ options[:default].call
51
+ else
52
+ options[:default]
53
+ end
36
54
 
37
- options[:default]
55
+ cast_value(value)
38
56
  end
39
57
 
40
58
  def render_nil?
41
59
  options.fetch(:render_nil, false)
42
60
  end
43
61
 
62
+ def enum_values
63
+ @options.key?(:values) ? @options[:values] : []
64
+ end
65
+
66
+ # Check if the value to be assigned is valid for the attribute
67
+ #
68
+ # Currently there are 2 validations
69
+ # 1. Value should be from the values list if they are defined
70
+ # e.g values: ["foo", "bar"] is set then any other value for this
71
+ # attribute will raise `Lutaml::Model::InvalidValueError`
72
+ #
73
+ # 2. Value count should be between the collection range if defined
74
+ # e.g if collection: 0..5 is set then the value greater then 5
75
+ # will raise `Lutaml::Model::CollectionCountOutOfRangeError`
76
+ def validate_value!(value)
77
+ valid_value!(value)
78
+ valid_collection!(value)
79
+ end
80
+
81
+ def valid_value!(value)
82
+ return true if value.nil? && !collection?
83
+ return true if enum_values.empty?
84
+
85
+ unless valid_value?(value)
86
+ raise Lutaml::Model::InvalidValueError.new(name, value, enum_values)
87
+ end
88
+
89
+ true
90
+ end
91
+
92
+ def valid_value?(value)
93
+ return true unless options[:values]
94
+
95
+ options[:values].include?(value)
96
+ end
97
+
98
+ def validate_value!(value)
99
+ # return true if none of the validations are present
100
+ return true if enum_values.empty? && singular?
101
+
102
+ # Use the default value if the value is nil
103
+ value = default if value.nil?
104
+
105
+ valid_value!(value) && valid_collection!(value)
106
+ end
107
+
108
+ def validate_collection_range
109
+ range = @options[:collection]
110
+ return if range == true
111
+
112
+ unless range.is_a?(Range)
113
+ raise ArgumentError, "Invalid collection range: #{range}"
114
+ end
115
+
116
+ if range.begin.nil?
117
+ raise ArgumentError,
118
+ "Invalid collection range: #{range}. Begin must be specified."
119
+ end
120
+
121
+ if range.begin.negative?
122
+ raise ArgumentError,
123
+ "Invalid collection range: #{range}. Begin must be non-negative."
124
+ end
125
+
126
+ if range.end && range.end < range.begin
127
+ raise ArgumentError,
128
+ "Invalid collection range: #{range}. End must be greater than or equal to begin."
129
+ end
130
+ end
131
+
132
+ def valid_collection!(value)
133
+ return true unless collection?
134
+
135
+ # Allow nil values for collections during initialization
136
+ return true if value.nil?
137
+
138
+ # Allow any value for unbounded collections
139
+ return true if options[:collection] == true
140
+
141
+ unless value.is_a?(Array)
142
+ raise Lutaml::Model::CollectionCountOutOfRangeError.new(
143
+ name,
144
+ value,
145
+ options[:collection],
146
+ )
147
+ end
148
+
149
+ range = options[:collection]
150
+ return true unless range.is_a?(Range)
151
+
152
+ if range.end.nil?
153
+ if value.size < range.begin
154
+ raise Lutaml::Model::CollectionCountOutOfRangeError.new(
155
+ name,
156
+ value,
157
+ range,
158
+ )
159
+ end
160
+ elsif !range.cover?(value.size)
161
+ raise Lutaml::Model::CollectionCountOutOfRangeError.new(
162
+ name,
163
+ value,
164
+ range,
165
+ )
166
+ end
167
+ end
168
+
44
169
  def serialize(value, format, options = {})
45
170
  if value.is_a?(Array)
46
171
  value.map do |v|