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.
- 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|
|