lutaml-model 0.3.9 → 0.3.10

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12f6b0d44c864c56f2573bae9a712485e0b266120fe53f342671b8cb144ce022
4
- data.tar.gz: 3facbc5cc6ef8149f1628415f3d511c2e010de28f8ab5968f2e436eb6aa41b39
3
+ metadata.gz: 5140f93f22f4b05222edc408aeca50552082e447b71fddd724b34ba9e6b82d55
4
+ data.tar.gz: 0aa44a3c72b65cca0ed2b7411614d8cdb0c44b4e8293e183935cb45ba7818d7c
5
5
  SHA512:
6
- metadata.gz: 0157ca6aa7a9e0e0368fd1da623068670d033a54385b91aa45082412512f5ee1643d72da168d5b68fdef531034f49363417dbfce31c4b47452c8f1d0136d839d
7
- data.tar.gz: cf6c69e91f9eb13275a845ee4253809cc60599527a26f28d13942fa26b10ce65bfbc6a6fed8fbc946a721a1d07edd8b98289447c668f7d8cba6dc3cd001bc85b
6
+ metadata.gz: 69e4d0d0a3a5bd19e11a43fd33e03566ca035d18cf948a0a2fc5abe5f880489e4a6ad347d8118a04c35c8c95be5b7eb29ca93d426643c7790ab045db2b34721c
7
+ data.tar.gz: df1699ba6647f53f97735d0986b92bf8f76d6bf44430117406100b257f5934ac4a7baa6a4243bb8384db277ecb975bf64efe2f1a16b293739b245150e800cb15
data/.rubocop_todo.yml CHANGED
@@ -1,27 +1,26 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2024-09-04 07:55:32 UTC using RuboCop version 1.66.0.
3
+ # on 2024-09-10 23:53:08 UTC using RuboCop version 1.66.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 1
10
- # This cop supports safe autocorrection (--autocorrect).
11
- # Configuration parameters: Severity, Include.
12
- # Include: **/*.gemspec
13
- Gemspec/RequireMFA:
14
- Exclude:
15
- - 'lutaml-model.gemspec'
16
-
17
- # Offense count: 62
9
+ # Offense count: 88
18
10
  # This cop supports safe autocorrection (--autocorrect).
19
11
  # Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
20
12
  # URISchemes: http, https
21
13
  Layout/LineLength:
22
14
  Enabled: false
23
15
 
24
- # Offense count: 10
16
+ # Offense count: 1
17
+ # This cop supports safe autocorrection (--autocorrect).
18
+ # Configuration parameters: AllowInHeredoc.
19
+ Layout/TrailingWhitespace:
20
+ Exclude:
21
+ - 'lib/lutaml/model/schema_location.rb'
22
+
23
+ # Offense count: 11
25
24
  # Configuration parameters: AllowedMethods.
26
25
  # AllowedMethods: enums
27
26
  Lint/ConstantDefinitionInBlock:
@@ -30,12 +29,19 @@ Lint/ConstantDefinitionInBlock:
30
29
  - 'spec/lutaml/model/schema/relaxng_schema_spec.rb'
31
30
  - 'spec/lutaml/model/schema/xsd_schema_spec.rb'
32
31
  - 'spec/lutaml/model/schema/yaml_schema_spec.rb'
32
+ - 'spec/lutaml/model/validation_spec.rb'
33
33
  - 'spec/lutaml/model/xml_adapter/xml_namespace_spec.rb'
34
34
 
35
- # Offense count: 29
35
+ # Offense count: 1
36
+ Lint/DuplicateMethods:
37
+ Exclude:
38
+ - 'lib/lutaml/model/attribute.rb'
39
+
40
+ # Offense count: 31
36
41
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
37
42
  Metrics/AbcSize:
38
43
  Exclude:
44
+ - 'lib/lutaml/model/attribute.rb'
39
45
  - 'lib/lutaml/model/comparable_model.rb'
40
46
  - 'lib/lutaml/model/schema/relaxng_schema.rb'
41
47
  - 'lib/lutaml/model/schema/xsd_schema.rb'
@@ -51,7 +57,7 @@ Metrics/AbcSize:
51
57
  Metrics/BlockLength:
52
58
  Max: 42
53
59
 
54
- # Offense count: 22
60
+ # Offense count: 25
55
61
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
56
62
  Metrics/CyclomaticComplexity:
57
63
  Exclude:
@@ -63,17 +69,17 @@ Metrics/CyclomaticComplexity:
63
69
  - 'lib/lutaml/model/xml_adapter/ox_adapter.rb'
64
70
  - 'lib/lutaml/model/xml_adapter/xml_document.rb'
65
71
 
66
- # Offense count: 36
72
+ # Offense count: 39
67
73
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
68
74
  Metrics/MethodLength:
69
- Max: 43
75
+ Max: 49
70
76
 
71
77
  # Offense count: 4
72
78
  # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
73
79
  Metrics/ParameterLists:
74
80
  Max: 9
75
81
 
76
- # Offense count: 18
82
+ # Offense count: 21
77
83
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
78
84
  Metrics/PerceivedComplexity:
79
85
  Exclude:
@@ -94,7 +100,7 @@ RSpec/ContextWording:
94
100
  - 'spec/lutaml/model/xml_adapter/ox_adapter_spec.rb'
95
101
  - 'spec/lutaml/model/xml_adapter/xml_namespace_spec.rb'
96
102
 
97
- # Offense count: 86
103
+ # Offense count: 101
98
104
  # Configuration parameters: CountAsOne.
99
105
  RSpec/ExampleLength:
100
106
  Max: 57
@@ -105,13 +111,14 @@ RSpec/IndexedLet:
105
111
  Exclude:
106
112
  - 'spec/address_spec.rb'
107
113
 
108
- # Offense count: 18
114
+ # Offense count: 19
109
115
  RSpec/LeakyConstantDeclaration:
110
116
  Exclude:
111
117
  - 'spec/lutaml/model/schema/json_schema_spec.rb'
112
118
  - 'spec/lutaml/model/schema/relaxng_schema_spec.rb'
113
119
  - 'spec/lutaml/model/schema/xsd_schema_spec.rb'
114
120
  - 'spec/lutaml/model/schema/yaml_schema_spec.rb'
121
+ - 'spec/lutaml/model/validation_spec.rb'
115
122
  - 'spec/lutaml/model/xml_adapter/xml_namespace_spec.rb'
116
123
 
117
124
  # Offense count: 4
@@ -122,23 +129,29 @@ RSpec/MultipleDescribes:
122
129
  - 'spec/lutaml/model/xml_adapter/xml_namespace_spec.rb'
123
130
  - 'spec/lutaml/model/xml_adapter_spec.rb'
124
131
 
125
- # Offense count: 75
132
+ # Offense count: 88
126
133
  RSpec/MultipleExpectations:
127
134
  Max: 11
128
135
 
129
- # Offense count: 11
136
+ # Offense count: 17
130
137
  # Configuration parameters: AllowSubject.
131
138
  RSpec/MultipleMemoizedHelpers:
132
139
  Max: 9
133
140
 
134
- # Offense count: 4
141
+ # Offense count: 7
135
142
  RSpec/PendingWithoutReason:
136
143
  Exclude:
137
144
  - 'spec/lutaml/model/mixed_content_spec.rb'
145
+ - 'spec/lutaml/model/validation_spec.rb'
138
146
  - 'spec/lutaml/model/xml_adapter/oga_adapter_spec.rb'
139
147
  - 'spec/lutaml/model/xml_adapter/xml_namespace_spec.rb'
140
148
  - 'spec/lutaml/model/xml_adapter_spec.rb'
141
149
 
150
+ # Offense count: 2
151
+ RSpec/RepeatedExampleGroupDescription:
152
+ Exclude:
153
+ - 'spec/lutaml/model/collection_spec.rb'
154
+
142
155
  # Offense count: 1
143
156
  # Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata.
144
157
  # Include: **/*_spec.rb
@@ -152,6 +165,11 @@ Security/CompoundHash:
152
165
  Exclude:
153
166
  - 'lib/lutaml/model/comparable_model.rb'
154
167
 
168
+ # Offense count: 1
169
+ Style/MissingRespondToMissing:
170
+ Exclude:
171
+ - 'lib/lutaml/model/serialize.rb'
172
+
155
173
  # Offense count: 1
156
174
  # Configuration parameters: AllowedMethods.
157
175
  # AllowedMethods: respond_to_missing?
data/README.adoc CHANGED
@@ -204,11 +204,45 @@ end
204
204
  Define attributes as collections (arrays or hashes) to store multiple values
205
205
  using the `collection` option.
206
206
 
207
+ `collection` can be set to:
208
+
209
+ `true`:::
210
+ The attribute contains an unbounded collection of objects of the declared class.
211
+
212
+ `{min}..{max}`:::
213
+ The attribute contains a collection of objects of the declared class with a
214
+ count within the specified range.
215
+ If the number of objects is out of this numbered range,
216
+ `CollectionCountOutOfRangeError` will be raised.
217
+ +
218
+ [example]
219
+ ====
220
+ When set to `0..1`, it means that the attribute is optional, it could be empty
221
+ or contain one object of the declared class.
222
+ ====
223
+ +
224
+ [example]
225
+ ====
226
+ When set to `1..` (equivalent to `1..Infinity`), it means that the
227
+ attribute must contain at least one object of the declared class and can contain
228
+ any number of objects.
229
+ ====
230
+ +
231
+ [example]
232
+ ====
233
+ When set to 5..10` means that there is a minimum of 5 and a maximum of 10
234
+ objects of the declared class. If the count of values for the attribute is less
235
+ then 5 or greater then 10, the `CollectionCountOutOfRangeError` will be raised.
236
+ ====
237
+
238
+
207
239
  Syntax:
208
240
 
209
241
  [source,ruby]
210
242
  ----
211
243
  attribute :name_of_attribute, Type, collection: true
244
+ attribute :name_of_attribute, Type, collection: {min}..{max}
245
+ attribute :name_of_attribute, Type, collection: {min}..
212
246
  ----
213
247
 
214
248
  .Using the `collection` option to define a collection attribute
@@ -219,18 +253,27 @@ attribute :name_of_attribute, Type, collection: true
219
253
  class Studio < Lutaml::Model::Serializable
220
254
  attribute :location, :string
221
255
  attribute :potters, :string, collection: true
256
+ attribute :address, :string, collection: 1..2
257
+ attribute :hobbies, :string, collection: 0..
222
258
  end
223
259
  ----
224
260
 
225
261
  [source,ruby]
226
262
  ----
227
- > Studio.new.potters
263
+ > Studio.new
264
+ > # address count is `0`, must be between 1 and 2 (Lutaml::Model::CollectionCountOutOfRangeError)
265
+ > Studio.new({ address: ["address 1", "address 2", "address 3"] })
266
+ > # address count is `3`, must be between 1 and 2 (Lutaml::Model::CollectionCountOutOfRangeError)
267
+ > Studio.new({ address: ["address 1"] }).potters
228
268
  > # []
229
- > Studio.new(potters: ['John Doe', 'Jane Doe']).potters
269
+ > Studio.new({ address: ["address 1"] }).address
270
+ > # ["address 1"]
271
+ > Studio.new(address: ["address 1"], potters: ['John Doe', 'Jane Doe']).potters
230
272
  > # ['John Doe', 'Jane Doe']
231
273
  ----
232
274
  ====
233
275
 
276
+
234
277
  [[attribute-enumeration]]
235
278
  === Attribute as an enumeration
236
279
 
@@ -947,6 +990,125 @@ end
947
990
  TODO: How to create mixed content from `#new`?
948
991
 
949
992
 
993
+ ==== Automatic support of `xsi:schemaLocation`
994
+
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`.
999
+
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.
1004
+
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]
1016
+ ----
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>
1028
+ ----
1029
+
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
1035
+ [example]
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
+
1041
+ [source,ruby]
1042
+ ----
1043
+ class Ceramic < Lutaml::Model::Serializable
1044
+ attribute :type, :string
1045
+ attribute :glaze, :string
1046
+ attribute :color, :string
1047
+
1048
+ xml do
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'
1054
+ end
1055
+ end
1056
+
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>
1105
+ ----
1106
+ ====
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
+
950
1112
 
951
1113
  === Key value data models
952
1114
 
@@ -1333,12 +1495,12 @@ class CustomCeramic < Lutaml::Model::Serializable
1333
1495
  map 'size', to: :size
1334
1496
  end
1335
1497
 
1336
- def name_to_json(model, value)
1337
- doc["name"] = "Masterpiece: #{value}"
1498
+ def name_to_json(model, doc)
1499
+ doc["name"] = "Masterpiece: #{model.name}"
1338
1500
  end
1339
1501
 
1340
- def name_from_json(model, doc)
1341
- model.name = value.sub(/^JSON Masterpiece: /, '')
1502
+ def name_from_json(model, value)
1503
+ model.name = value.sub(/^Masterpiece: /, '')
1342
1504
  end
1343
1505
  end
1344
1506
  ----
@@ -1547,6 +1709,101 @@ In this example:
1547
1709
  ====
1548
1710
 
1549
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
+
1550
1807
  == Adapters
1551
1808
 
1552
1809
  === General
@@ -6,11 +6,11 @@ module Lutaml
6
6
  def initialize(name, type, options = {})
7
7
  @name = name
8
8
  @type = cast_type(type)
9
-
10
9
  @options = options
11
10
 
12
- if collection? && !options[:default]
13
- @options[:default] = -> { [] }
11
+ if collection?
12
+ validate_collection_range
13
+ @options[:default] = -> { [] } unless options[:default]
14
14
  end
15
15
  end
16
16
 
@@ -31,6 +31,10 @@ module Lutaml
31
31
  options[:collection] || false
32
32
  end
33
33
 
34
+ def singular?
35
+ !collection?
36
+ end
37
+
34
38
  def default
35
39
  return options[:default].call if options[:default].is_a?(Proc)
36
40
 
@@ -41,6 +45,113 @@ module Lutaml
41
45
  options.fetch(:render_nil, false)
42
46
  end
43
47
 
48
+ def enum_values
49
+ @options.key?(:values) ? @options[:values] : []
50
+ end
51
+
52
+ # Check if the value to be assigned is valid for the attribute
53
+ #
54
+ # Currently there are 2 validations
55
+ # 1. Value should be from the values list if they are defined
56
+ # e.g values: ["foo", "bar"] is set then any other value for this
57
+ # attribute will raise `Lutaml::Model::InvalidValueError`
58
+ #
59
+ # 2. Value count should be between the collection range if defined
60
+ # e.g if collection: 0..5 is set then the value greater then 5
61
+ # will raise `Lutaml::Model::CollectionCountOutOfRangeError`
62
+ def validate_value!(value)
63
+ valid_value!(value)
64
+ valid_collection!(value)
65
+ end
66
+
67
+ def valid_value!(value)
68
+ return true if value.nil? && !collection?
69
+ return true if enum_values.empty?
70
+
71
+ unless valid_value?(value)
72
+ raise Lutaml::Model::InvalidValueError.new(name, value, enum_values)
73
+ end
74
+
75
+ true
76
+ end
77
+
78
+ def valid_value?(value)
79
+ return true unless options[:values]
80
+
81
+ options[:values].include?(value)
82
+ end
83
+
84
+ def validate_value!(value)
85
+ # return true if none of the validations are present
86
+ return true if enum_values.empty? && singular?
87
+
88
+ # Use the default value if the value is nil
89
+ value = default if value.nil?
90
+
91
+ valid_value!(value) && valid_collection!(value)
92
+ end
93
+
94
+ def validate_collection_range
95
+ range = @options[:collection]
96
+ return if range == true
97
+
98
+ unless range.is_a?(Range)
99
+ raise ArgumentError, "Invalid collection range: #{range}"
100
+ end
101
+
102
+ if range.begin.nil?
103
+ raise ArgumentError,
104
+ "Invalid collection range: #{range}. Begin must be specified."
105
+ end
106
+
107
+ if range.begin.negative?
108
+ raise ArgumentError,
109
+ "Invalid collection range: #{range}. Begin must be non-negative."
110
+ end
111
+
112
+ if range.end && range.end < range.begin
113
+ raise ArgumentError,
114
+ "Invalid collection range: #{range}. End must be greater than or equal to begin."
115
+ end
116
+ end
117
+
118
+ def valid_collection!(value)
119
+ return true unless collection?
120
+
121
+ # Allow nil values for collections during initialization
122
+ return true if value.nil?
123
+
124
+ # Allow any value for unbounded collections
125
+ return true if options[:collection] == true
126
+
127
+ unless value.is_a?(Array)
128
+ raise Lutaml::Model::CollectionCountOutOfRangeError.new(
129
+ name,
130
+ value,
131
+ options[:collection],
132
+ )
133
+ end
134
+
135
+ range = options[:collection]
136
+ return true unless range.is_a?(Range)
137
+
138
+ if range.end.nil?
139
+ if value.size < range.begin
140
+ raise Lutaml::Model::CollectionCountOutOfRangeError.new(
141
+ name,
142
+ value,
143
+ range,
144
+ )
145
+ end
146
+ elsif !range.cover?(value.size)
147
+ raise Lutaml::Model::CollectionCountOutOfRangeError.new(
148
+ name,
149
+ value,
150
+ range,
151
+ )
152
+ end
153
+ end
154
+
44
155
  def serialize(value, format, options = {})
45
156
  if value.is_a?(Array)
46
157
  value.map do |v|
@@ -0,0 +1,29 @@
1
+ module Lutaml
2
+ module Model
3
+ class CollectionCountOutOfRangeError < Error
4
+ def initialize(attr_name, value, range)
5
+ @attr_name = attr_name
6
+ @value = value
7
+ @range = range
8
+
9
+ super()
10
+ end
11
+
12
+ def to_s
13
+ "#{@attr_name} count is #{@value.size}, must be #{range_to_string}"
14
+ end
15
+
16
+ private
17
+
18
+ def range_to_string
19
+ if @range.end.nil?
20
+ "at least #{@range.begin}"
21
+ elsif @range.begin == @range.end
22
+ "exactly #{@range.begin}"
23
+ else
24
+ "between #{@range.begin} and #{@range.end}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # lib/lutaml/model/error/validation_error.rb
2
+ module Lutaml
3
+ module Model
4
+ class ValidationError < Error
5
+ attr_reader :errors
6
+
7
+ def initialize(errors)
8
+ @errors = errors
9
+ super(errors.join(", "))
10
+ end
11
+
12
+ def include?(error_class)
13
+ errors.any?(error_class)
14
+ end
15
+
16
+ def error_messages
17
+ errors.map(&:message)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -7,3 +7,5 @@ end
7
7
 
8
8
  require_relative "error/invalid_value_error"
9
9
  require_relative "error/unknown_adapter_type_error"
10
+ require_relative "error/collection_count_out_of_range_error"
11
+ require_relative "error/validation_error"
@@ -0,0 +1,59 @@
1
+ module Lutaml
2
+ module Model
3
+ class Location
4
+ attr_reader :namespace, :location
5
+
6
+ def initialize(namespace:, location:)
7
+ @namespace = namespace
8
+ @location = location
9
+ end
10
+
11
+ def to_xml_attribute
12
+ "#{@namespace} #{@location}".strip
13
+ end
14
+ end
15
+
16
+ class SchemaLocation
17
+ DEFAULT_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance".freeze
18
+
19
+ attr_reader :namespace, :prefix, :schema_location
20
+
21
+ def initialize(schema_location:, prefix: "xsi",
22
+ namespace: DEFAULT_NAMESPACE)
23
+ @original_schema_location = schema_location
24
+ @schema_location = parsed_schema_locations(schema_location)
25
+ @prefix = prefix
26
+ @namespace = namespace
27
+ end
28
+
29
+ def to_xml_attributes
30
+ {
31
+ "xmlns:#{prefix}" => namespace,
32
+ "#{prefix}:schemaLocation" => schema_location.map(&:to_xml_attribute).join(" "),
33
+ }
34
+ end
35
+
36
+ def [](index)
37
+ @schema_location[index]
38
+ end
39
+
40
+ def size
41
+ @schema_location.size
42
+ end
43
+
44
+ private
45
+
46
+ def parsed_schema_locations(schema_location)
47
+ locations = if schema_location.is_a?(Hash)
48
+ schema_location
49
+ else
50
+ schema_location.split.each_slice(2)
51
+ end
52
+
53
+ locations.map do |n, l|
54
+ Location.new(namespace: n, location: l)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -9,11 +9,14 @@ require_relative "xml_mapping"
9
9
  require_relative "key_value_mapping"
10
10
  require_relative "json_adapter"
11
11
  require_relative "comparable_model"
12
+ require_relative "schema_location"
13
+ require_relative "validation"
12
14
 
13
15
  module Lutaml
14
16
  module Model
15
17
  module Serialize
16
18
  include ComparableModel
19
+ include Validation
17
20
 
18
21
  def self.included(base)
19
22
  base.extend(ClassMethods)
@@ -52,25 +55,10 @@ module Lutaml
52
55
 
53
56
  define_method(:"#{name}=") do |value|
54
57
  instance_variable_set(:"@#{name}", value)
55
- validate
58
+ # validate!(name)
56
59
  end
57
60
  end
58
61
 
59
- # Check if the value to be assigned is valid for the attribute
60
- def attr_value_valid?(name, value)
61
- attr = attributes[name]
62
-
63
- return true unless attr.options[:values]
64
-
65
- # Allow nil values if there's no default
66
- return true if value.nil? && !attr.default
67
-
68
- # Use the default value if the value is nil
69
- value = attr.default if value.nil?
70
-
71
- attr.options[:values].include?(value)
72
- end
73
-
74
62
  Lutaml::Model::Config::AVAILABLE_FORMATS.each do |format|
75
63
  define_method(format) do |&block|
76
64
  klass = format == :xml ? XmlMapping : KeyValueMapping
@@ -92,8 +80,8 @@ module Lutaml
92
80
  define_method(:"of_#{format}") do |hash|
93
81
  if hash.is_a?(Array)
94
82
  return hash.map do |item|
95
- apply_mappings(item, format)
96
- end
83
+ apply_mappings(item, format)
84
+ end
97
85
  end
98
86
 
99
87
  apply_mappings(hash, format)
@@ -316,6 +304,14 @@ module Lutaml
316
304
  instance.ordered = mappings_for(:xml).mixed_content? || options[:mixed_content]
317
305
  end
318
306
 
307
+ if doc["__schema_location"]
308
+ instance.schema_location = Lutaml::Model::SchemaLocation.new(
309
+ schema_location: doc["__schema_location"][:schema_location],
310
+ prefix: doc["__schema_location"][:prefix],
311
+ namespace: doc["__schema_location"][:namespace],
312
+ )
313
+ end
314
+
319
315
  mappings.each do |rule|
320
316
  attr = attributes[rule.to]
321
317
  raise "Attribute '#{rule.to}' not found in #{self}" unless attr
@@ -376,9 +372,11 @@ module Lutaml
376
372
  end
377
373
  end
378
374
 
379
- attr_accessor :element_order
375
+ attr_accessor :element_order, :schema_location
380
376
 
381
377
  def initialize(attrs = {})
378
+ @validate_on_set = attrs.delete(:validate_on_set) || false
379
+
382
380
  return unless self.class.attributes
383
381
 
384
382
  if attrs.is_a?(Lutaml::Model::MappingHash)
@@ -386,13 +384,41 @@ module Lutaml
386
384
  @element_order = attrs.item_order
387
385
  end
388
386
 
387
+ if attrs.key?(:schema_location)
388
+ self.schema_location = attrs[:schema_location]
389
+ end
390
+
389
391
  self.class.attributes.each do |name, attr|
390
- value = self.class.attr_value(attrs, name, attr)
392
+ value = if attrs.key?(name) || attrs.key?(name.to_s)
393
+ self.class.attr_value(attrs, name, attr)
394
+ else
395
+ attr.default
396
+ end
391
397
 
392
- send(:"#{name}=", self.class.ensure_utf8(value))
398
+ # Initialize collections with an empty array if no value is provided
399
+ if attr.collection? && value.nil?
400
+ value = []
401
+ end
402
+
403
+ instance_variable_set(:"@#{name}", self.class.ensure_utf8(value))
393
404
  end
405
+ end
394
406
 
395
- validate
407
+ def method_missing(method_name, *args)
408
+ if method_name.to_s.end_with?("=") && self.class.attributes.key?(method_name.to_s.chomp("=").to_sym)
409
+ define_singleton_method(method_name) do |value|
410
+ instance_variable_set(:"@#{method_name.to_s.chomp('=')}", value)
411
+ end
412
+ send(method_name, *args)
413
+ else
414
+ super
415
+ end
416
+ end
417
+
418
+ def validate_attribute!(attr_name)
419
+ attr = self.class.attributes[attr_name]
420
+ value = instance_variable_get(:"@#{attr_name}")
421
+ attr.validate_value!(value)
396
422
  end
397
423
 
398
424
  def ordered?
@@ -413,7 +439,7 @@ module Lutaml
413
439
 
414
440
  Lutaml::Model::Config::AVAILABLE_FORMATS.each do |format|
415
441
  define_method(:"to_#{format}") do |options = {}|
416
- validate
442
+ validate!
417
443
  adapter = Lutaml::Model::Config.public_send(:"#{format}_adapter")
418
444
  representation = if format == :xml
419
445
  self
@@ -425,16 +451,6 @@ module Lutaml
425
451
  adapter.new(representation).public_send(:"to_#{format}", options)
426
452
  end
427
453
  end
428
-
429
- def validate
430
- self.class.attributes.each do |name, attr|
431
- value = send(name)
432
- unless self.class.attr_value_valid?(name, value)
433
- raise Lutaml::Model::InvalidValueError.new(name, value,
434
- attr.options[:values])
435
- end
436
- end
437
- end
438
454
  end
439
455
  end
440
456
  end
@@ -0,0 +1,24 @@
1
+ module Lutaml
2
+ module Model
3
+ module Validation
4
+ def validate
5
+ errors = []
6
+ self.class.attributes.each do |name, attr|
7
+ value = instance_variable_get(:"@#{name}")
8
+ begin
9
+ attr.validate_value!(value)
10
+ rescue Lutaml::Model::InvalidValueError,
11
+ Lutaml::Model::CollectionCountOutOfRangeError => e
12
+ errors << e
13
+ end
14
+ end
15
+ errors
16
+ end
17
+
18
+ def validate!
19
+ errors = validate
20
+ raise Lutaml::Model::ValidationError.new(errors) if errors.any?
21
+ end
22
+ end
23
+ end
24
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.3.9"
5
+ VERSION = "0.3.10"
6
6
  end
7
7
  end
@@ -77,16 +77,23 @@ module Lutaml
77
77
  element.children.each_with_object(result) do |child, hash|
78
78
  value = child.text? ? child.text : parse_element(child)
79
79
 
80
- if hash[child.unprefixed_name]
81
- hash[child.unprefixed_name] =
82
- [hash[child.unprefixed_name], value].flatten
83
- else
84
- hash[child.unprefixed_name] = value
85
- end
80
+ hash[child.unprefixed_name] = if hash[child.unprefixed_name]
81
+ [hash[child.unprefixed_name], value].flatten
82
+ else
83
+ value
84
+ end
86
85
  end
87
86
 
88
87
  element.attributes.each_value do |attr|
89
- result[attr.unprefixed_name] = attr.value
88
+ if attr.unprefixed_name == "schemaLocation"
89
+ result["__schema_location"] = {
90
+ namespace: attr.namespace,
91
+ prefix: attr.namespace_prefix,
92
+ schema_location: attr.value,
93
+ }
94
+ else
95
+ result[attr.unprefixed_name] = attr.value
96
+ end
90
97
  end
91
98
 
92
99
  result
@@ -150,6 +157,9 @@ module Lutaml
150
157
  attributes = options[:xml_attributes] ||= {}
151
158
  attributes = build_attributes(element,
152
159
  xml_mapping, options).merge(attributes)&.compact
160
+ if element.respond_to?(:schema_location) && element.schema_location
161
+ attributes.merge!(element.schema_location.to_xml_attributes)
162
+ end
153
163
 
154
164
  prefix = if options.key?(:namespace_prefix)
155
165
  options[:namespace_prefix]
data/lutaml-model.gemspec CHANGED
@@ -32,4 +32,5 @@ Gem::Specification.new do |spec|
32
32
 
33
33
  spec.add_dependency "bigdecimal"
34
34
  spec.add_dependency "thor"
35
+ spec.metadata["rubygems_mfa_required"] = "true"
35
36
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.9
4
+ version: 0.3.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-04 00:00:00.000000000 Z
11
+ date: 2024-09-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal
@@ -69,8 +69,10 @@ files:
69
69
  - lib/lutaml/model/comparison.rb
70
70
  - lib/lutaml/model/config.rb
71
71
  - lib/lutaml/model/error.rb
72
+ - lib/lutaml/model/error/collection_count_out_of_range_error.rb
72
73
  - lib/lutaml/model/error/invalid_value_error.rb
73
74
  - lib/lutaml/model/error/unknown_adapter_type_error.rb
75
+ - lib/lutaml/model/error/validation_error.rb
74
76
  - lib/lutaml/model/json_adapter.rb
75
77
  - lib/lutaml/model/json_adapter/json_document.rb
76
78
  - lib/lutaml/model/json_adapter/json_object.rb
@@ -86,6 +88,7 @@ files:
86
88
  - lib/lutaml/model/schema/relaxng_schema.rb
87
89
  - lib/lutaml/model/schema/xsd_schema.rb
88
90
  - lib/lutaml/model/schema/yaml_schema.rb
91
+ - lib/lutaml/model/schema_location.rb
89
92
  - lib/lutaml/model/serializable.rb
90
93
  - lib/lutaml/model/serialize.rb
91
94
  - lib/lutaml/model/toml_adapter.rb
@@ -97,6 +100,7 @@ files:
97
100
  - lib/lutaml/model/type/date_time.rb
98
101
  - lib/lutaml/model/type/time_without_date.rb
99
102
  - lib/lutaml/model/utils.rb
103
+ - lib/lutaml/model/validation.rb
100
104
  - lib/lutaml/model/version.rb
101
105
  - lib/lutaml/model/xml_adapter.rb
102
106
  - lib/lutaml/model/xml_adapter/builder/nokogiri.rb
@@ -119,7 +123,8 @@ files:
119
123
  homepage: https://github.com/lutaml/lutaml-model
120
124
  licenses:
121
125
  - BSD-2-Clause
122
- metadata: {}
126
+ metadata:
127
+ rubygems_mfa_required: 'true'
123
128
  post_install_message:
124
129
  rdoc_options: []
125
130
  require_paths: