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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +47 -38
- data/README.adoc +323 -65
- data/lib/lutaml/model/attribute.rb +114 -3
- data/lib/lutaml/model/error/collection_count_out_of_range_error.rb +29 -0
- data/lib/lutaml/model/error/validation_error.rb +21 -0
- data/lib/lutaml/model/error.rb +2 -0
- data/lib/lutaml/model/schema_location.rb +59 -0
- data/lib/lutaml/model/serialize.rb +53 -34
- data/lib/lutaml/model/validation.rb +24 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model/xml_adapter/builder/ox.rb +13 -3
- data/lib/lutaml/model/xml_adapter/ox_adapter.rb +3 -0
- data/lib/lutaml/model/xml_adapter/xml_document.rb +26 -14
- data/lib/lutaml/model/xml_mapping.rb +1 -3
- data/lutaml-model.gemspec +2 -0
- metadata +22 -3
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
|
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(
|
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
|
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',
|
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
|
-
|
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
|
-
|
993
|
+
==== Automatic support of `xsi:schemaLocation`
|
904
994
|
|
905
|
-
|
906
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
913
|
-
|
914
|
-
|
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
|
-
|
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
|
923
|
-
attribute :
|
924
|
-
attribute :
|
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 '
|
939
|
-
|
940
|
-
map_element '
|
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
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
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,
|
1341
|
-
doc["name"] = "Masterpiece: #{
|
1498
|
+
def name_to_json(model, doc)
|
1499
|
+
doc["name"] = "Masterpiece: #{model.name}"
|
1342
1500
|
end
|
1343
1501
|
|
1344
|
-
def name_from_json(model,
|
1345
|
-
model.name = value.sub(/^
|
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?
|
13
|
-
|
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|
|