lutaml-model 0.3.9 → 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 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: