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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +43 -25
- data/Gemfile +1 -0
- data/LICENSE.md +33 -0
- data/README.adoc +384 -18
- data/lib/lutaml/model/attribute.rb +130 -5
- data/lib/lutaml/model/error/collection_count_out_of_range_error.rb +29 -0
- data/lib/lutaml/model/error/incorrect_mapping_argument_error.rb +6 -0
- data/lib/lutaml/model/error/type_not_enabled_error.rb +9 -0
- data/lib/lutaml/model/error/validation_error.rb +21 -0
- data/lib/lutaml/model/error.rb +4 -0
- data/lib/lutaml/model/json_adapter/standard_json_adapter.rb +1 -2
- data/lib/lutaml/model/key_value_mapping.rb +15 -1
- data/lib/lutaml/model/mapping_hash.rb +14 -0
- data/lib/lutaml/model/mapping_rule.rb +30 -6
- data/lib/lutaml/model/schema_location.rb +59 -0
- data/lib/lutaml/model/serialize.rb +135 -86
- data/lib/lutaml/model/type.rb +8 -0
- data/lib/lutaml/model/utils.rb +16 -0
- data/lib/lutaml/model/validation.rb +24 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model/xml_adapter/builder/nokogiri.rb +12 -2
- data/lib/lutaml/model/xml_adapter/builder/ox.rb +4 -0
- data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +2 -1
- data/lib/lutaml/model/xml_adapter/ox_adapter.rb +1 -0
- data/lib/lutaml/model/xml_adapter/xml_document.rb +60 -30
- data/lib/lutaml/model/xml_adapter/xml_namespace.rb +1 -1
- data/lib/lutaml/model/xml_mapping.rb +27 -5
- data/lib/lutaml/model/xml_mapping_rule.rb +3 -1
- data/lib/lutaml/model/yaml_adapter/standard_yaml_adapter.rb +10 -5
- data/lutaml-model.gemspec +1 -1
- metadata +11 -17
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
|
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(
|
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,
|
1337
|
-
doc["name"] = "Masterpiece: #{
|
1596
|
+
def name_to_json(model, doc)
|
1597
|
+
doc["name"] = "Masterpiece: #{model.name}"
|
1338
1598
|
end
|
1339
1599
|
|
1340
|
-
def name_from_json(model,
|
1341
|
-
model.name = value.sub(/^
|
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
|
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,
|
1866
|
-
attribute :description,
|
2231
|
+
attribute :length, :integer
|
2232
|
+
attribute :description, :string
|
1867
2233
|
end
|
1868
2234
|
----
|
1869
2235
|
|
1870
2236
|
[NOTE]
|
1871
2237
|
====
|
1872
|
-
`Lutaml::Model`
|
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,
|
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?
|
13
|
-
|
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
|
-
|
49
|
+
value = if options[:default].is_a?(Proc)
|
50
|
+
options[:default].call
|
51
|
+
else
|
52
|
+
options[:default]
|
53
|
+
end
|
36
54
|
|
37
|
-
|
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|
|