lutaml-model 0.8.31 → 0.8.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.adoc +25 -23
- data/docs/_pages/breaking-changes.adoc +58 -0
- data/docs/_pages/collections.adoc +25 -24
- data/docs/_pages/validation.adoc +48 -1
- data/lib/compat/opal/lutaml_model_boot.rb +1 -0
- data/lib/lutaml/key_value/transform.rb +7 -1
- data/lib/lutaml/model/attribute.rb +67 -24
- data/lib/lutaml/model/collection.rb +5 -2
- data/lib/lutaml/model/collection_handler.rb +45 -2
- data/lib/lutaml/model/serialize/enum_handling.rb +5 -1
- data/lib/lutaml/model/validation.rb +49 -4
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/format.rb +2 -0
- data/lib/lutaml/xml/model_transform.rb +33 -9
- data/lib/lutaml/xml/nested_collection_attribute.rb +95 -0
- data/lib/lutaml/xml/schema/xsd_schema.rb +396 -128
- data/lib/lutaml/xml/serialization/instance_methods.rb +2 -2
- data/lib/lutaml/xml/transformation/element_builder.rb +55 -8
- data/spec/lutaml/model/attribute_collection_spec.rb +156 -0
- data/spec/lutaml/model/attribute_spec.rb +1 -1
- data/spec/lutaml/model/cardinality_strict_spec.rb +614 -0
- data/spec/lutaml/model/cdata_spec.rb +1 -1
- data/spec/lutaml/model/collection_register_spec.rb +119 -0
- data/spec/lutaml/model/collection_single_value_coercion_spec.rb +222 -0
- data/spec/lutaml/model/root_mappings_spec.rb +10 -7
- data/spec/lutaml/model/schema/xsd_schema_spec.rb +785 -25
- data/spec/lutaml/model/serializable_validation_spec.rb +293 -0
- data/spec/lutaml/model/value_policy_spec.rb +6 -3
- data/spec/lutaml/model/xsd_form_default_patterns_spec.rb +12 -12
- data/spec/lutaml/model/xsd_patterns_spec.rb +29 -29
- data/spec/lutaml/model/xsd_type_validation_spec.rb +1 -1
- data/spec/lutaml/turtle/transform_spec.rb +2 -2
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a0c1ee735ac6b12da331e1f43dc6617d05cfe18534dc5255aaba0c789ee601e
|
|
4
|
+
data.tar.gz: bde4936c85167bcec7a4618cd8e91c9c4cf65d2feb97b0d94c78bf368f085248
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 773ec772915ec1fd0645d68fa2e62c50bea3af147b19c67c22e5bd16cf1f6dec2a00bc1ff1945e612a7a6f5d22ce404d3e9687fead480d54390a847908e6d804
|
|
7
|
+
data.tar.gz: 318c31871e7bbb83bd1bea6547f3dbeda4e1768da9d44f0e85c7434bd8db15ade859bdf9a909771126b33a85e345bc1e803a93eacdec2e557a51d39bf0e6f8cb
|
data/README.adoc
CHANGED
|
@@ -4519,8 +4519,6 @@ end
|
|
|
4519
4519
|
|
|
4520
4520
|
=== Nested collections
|
|
4521
4521
|
|
|
4522
|
-
TODO: This case needs to be fixed.
|
|
4523
|
-
|
|
4524
4522
|
Collections can be nested within other models and define their own serialization
|
|
4525
4523
|
rules.
|
|
4526
4524
|
|
|
@@ -4533,38 +4531,42 @@ Data:
|
|
|
4533
4531
|
|
|
4534
4532
|
[source,xml]
|
|
4535
4533
|
----
|
|
4536
|
-
<
|
|
4537
|
-
<
|
|
4538
|
-
<
|
|
4539
|
-
<
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
<
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
<
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
</
|
|
4534
|
+
<bibitem>
|
|
4535
|
+
<titles>
|
|
4536
|
+
<title-group>
|
|
4537
|
+
<artifact>
|
|
4538
|
+
<content>Title One</content>
|
|
4539
|
+
</artifact>
|
|
4540
|
+
<artifact>
|
|
4541
|
+
<content>Title Two</content>
|
|
4542
|
+
</artifact>
|
|
4543
|
+
<artifact>
|
|
4544
|
+
<content>Title Three</content>
|
|
4545
|
+
</artifact>
|
|
4546
|
+
</title-group>
|
|
4547
|
+
</titles>
|
|
4548
|
+
</bibitem>
|
|
4549
4549
|
----
|
|
4550
4550
|
|
|
4551
4551
|
[source,yaml]
|
|
4552
4552
|
----
|
|
4553
4553
|
---
|
|
4554
4554
|
titles:
|
|
4555
|
-
|
|
4556
|
-
-
|
|
4557
|
-
|
|
4558
|
-
-
|
|
4559
|
-
content: Title Two
|
|
4560
|
-
- artifact:
|
|
4561
|
-
content: Title Three
|
|
4555
|
+
items:
|
|
4556
|
+
- content: Title One
|
|
4557
|
+
- content: Title Two
|
|
4558
|
+
- content: Title Three
|
|
4562
4559
|
----
|
|
4563
4560
|
|
|
4564
4561
|
[source,ruby]
|
|
4565
4562
|
----
|
|
4566
4563
|
class Title < Lutaml::Model::Serializable
|
|
4567
4564
|
attribute :content, :string
|
|
4565
|
+
|
|
4566
|
+
xml do
|
|
4567
|
+
element "artifact"
|
|
4568
|
+
map_element "content", to: :content
|
|
4569
|
+
end
|
|
4568
4570
|
end
|
|
4569
4571
|
|
|
4570
4572
|
class TitleCollection < Lutaml::Model::Collection
|
|
@@ -4581,7 +4583,7 @@ class BibItem < Lutaml::Model::Serializable
|
|
|
4581
4583
|
|
|
4582
4584
|
xml do
|
|
4583
4585
|
element "bibitem"
|
|
4584
|
-
# This
|
|
4586
|
+
# This wraps the collection's "title-group" root under "titles"
|
|
4585
4587
|
map_element "titles", to: :titles
|
|
4586
4588
|
end
|
|
4587
4589
|
end
|
|
@@ -143,6 +143,64 @@ end
|
|
|
143
143
|
Simply remove the `no_root` call - models without `element()` are type-only by default.
|
|
144
144
|
|
|
145
145
|
|
|
146
|
+
=== v0.9.0: Strict cardinality validation
|
|
147
|
+
|
|
148
|
+
Attributes now enforce their declared cardinality. An attribute without
|
|
149
|
+
`collection:` holds a single value, and giving it several is a violation that
|
|
150
|
+
`#validate` reports. See link:validation[Validation] for the full rules.
|
|
151
|
+
|
|
152
|
+
Four behavior changes come with it.
|
|
153
|
+
|
|
154
|
+
==== Nested models now run their own validations
|
|
155
|
+
|
|
156
|
+
`#validate` recurses into nested models and validates each one. Polymorphic,
|
|
157
|
+
required and custom checks on a nested model ran nowhere before; they run now.
|
|
158
|
+
A model that validated clean may start reporting errors its children always had.
|
|
159
|
+
|
|
160
|
+
==== Singular readers can return arrays
|
|
161
|
+
|
|
162
|
+
An attribute given more values than it was declared for keeps them all, so
|
|
163
|
+
`#validate` has something to report. Until the violation is fixed, the reader
|
|
164
|
+
returns an array rather than a single value.
|
|
165
|
+
|
|
166
|
+
[source,ruby]
|
|
167
|
+
----
|
|
168
|
+
obj = Person.from_xml("<person><name>A</name><name>B</name></person>")
|
|
169
|
+
obj.name # => ["A", "B"]
|
|
170
|
+
obj.validate # => [#<Lutaml::Model::CollectionTrueMissingError ...>]
|
|
171
|
+
----
|
|
172
|
+
|
|
173
|
+
Code that assumed a singular reader always returns a scalar should call
|
|
174
|
+
`#validate` before trusting the shape.
|
|
175
|
+
|
|
176
|
+
While the violation stands the document does not round-trip: serializing writes
|
|
177
|
+
the array out as a single stringified value. Fix the cardinality and output
|
|
178
|
+
returns to normal.
|
|
179
|
+
|
|
180
|
+
==== Enum shorthand predicates answer false while over-counted
|
|
181
|
+
|
|
182
|
+
The generated `value?` methods read through the same reader, so an attribute
|
|
183
|
+
holding two values answers `false` for all of them.
|
|
184
|
+
|
|
185
|
+
[source,ruby]
|
|
186
|
+
----
|
|
187
|
+
obj = Doc.from_json('{"kind": ["draft", "final"]}')
|
|
188
|
+
obj.draft? # => false, the attribute holds two values
|
|
189
|
+
obj.kind = "draft"
|
|
190
|
+
obj.draft? # => true
|
|
191
|
+
----
|
|
192
|
+
|
|
193
|
+
==== Assigning a scalar to a collection attribute wraps it
|
|
194
|
+
|
|
195
|
+
Assignment now produces the same shape as parsing.
|
|
196
|
+
|
|
197
|
+
[source,ruby]
|
|
198
|
+
----
|
|
199
|
+
doc.tags = "one"
|
|
200
|
+
doc.tags # => ["one"]
|
|
201
|
+
----
|
|
202
|
+
|
|
203
|
+
|
|
146
204
|
=== v0.8.0: XML default namespace behavior
|
|
147
205
|
|
|
148
206
|
Version 0.8.0 introduces a **breaking change** to XML namespace serialization
|
|
@@ -784,8 +784,6 @@ See link:../_guides/consolidation-mapping[Consolidation Mapping] for the full gu
|
|
|
784
784
|
|
|
785
785
|
=== Nested collections
|
|
786
786
|
|
|
787
|
-
TODO: This case needs to be fixed.
|
|
788
|
-
|
|
789
787
|
Collections can be nested within other models and define their own serialization
|
|
790
788
|
rules.
|
|
791
789
|
|
|
@@ -798,38 +796,42 @@ Data:
|
|
|
798
796
|
|
|
799
797
|
[source,xml]
|
|
800
798
|
----
|
|
801
|
-
<
|
|
802
|
-
<
|
|
803
|
-
<
|
|
804
|
-
<
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
<
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
<
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
</
|
|
799
|
+
<bibitem>
|
|
800
|
+
<titles>
|
|
801
|
+
<title-group>
|
|
802
|
+
<artifact>
|
|
803
|
+
<content>Title One</content>
|
|
804
|
+
</artifact>
|
|
805
|
+
<artifact>
|
|
806
|
+
<content>Title Two</content>
|
|
807
|
+
</artifact>
|
|
808
|
+
<artifact>
|
|
809
|
+
<content>Title Three</content>
|
|
810
|
+
</artifact>
|
|
811
|
+
</title-group>
|
|
812
|
+
</titles>
|
|
813
|
+
</bibitem>
|
|
814
814
|
----
|
|
815
815
|
|
|
816
816
|
[source,yaml]
|
|
817
817
|
----
|
|
818
818
|
---
|
|
819
819
|
titles:
|
|
820
|
-
|
|
821
|
-
-
|
|
822
|
-
|
|
823
|
-
-
|
|
824
|
-
content: Title Two
|
|
825
|
-
- artifact:
|
|
826
|
-
content: Title Three
|
|
820
|
+
items:
|
|
821
|
+
- content: Title One
|
|
822
|
+
- content: Title Two
|
|
823
|
+
- content: Title Three
|
|
827
824
|
----
|
|
828
825
|
|
|
829
826
|
[source,ruby]
|
|
830
827
|
----
|
|
831
828
|
class Title < Lutaml::Model::Serializable
|
|
832
829
|
attribute :content, :string
|
|
830
|
+
|
|
831
|
+
xml do
|
|
832
|
+
element "artifact"
|
|
833
|
+
map_element "content", to: :content
|
|
834
|
+
end
|
|
833
835
|
end
|
|
834
836
|
|
|
835
837
|
class TitleCollection < Lutaml::Model::Collection
|
|
@@ -846,7 +848,7 @@ class BibItem < Lutaml::Model::Serializable
|
|
|
846
848
|
|
|
847
849
|
xml do
|
|
848
850
|
element "bibitem"
|
|
849
|
-
# This
|
|
851
|
+
# This wraps the collection's "title-group" root under "titles"
|
|
850
852
|
map_element "titles", to: :titles
|
|
851
853
|
end
|
|
852
854
|
end
|
|
@@ -1667,4 +1669,3 @@ class ReferenceSet < Lutaml::Model::Collection
|
|
|
1667
1669
|
end
|
|
1668
1670
|
end
|
|
1669
1671
|
----
|
|
1670
|
-
|
data/docs/_pages/validation.adoc
CHANGED
|
@@ -25,14 +25,61 @@ Lutaml::Model supports the following validation types:
|
|
|
25
25
|
|
|
26
26
|
=== Collection validation
|
|
27
27
|
|
|
28
|
-
The `collection` option
|
|
28
|
+
The `collection` option sets an attribute's cardinality: `collection: true`
|
|
29
|
+
means `0..*`, a range such as `(1..)` or `(0..5)` bounds the count, and omitting
|
|
30
|
+
it (the default) means `0..1`, a single value.
|
|
29
31
|
|
|
30
32
|
[source,ruby]
|
|
31
33
|
----
|
|
32
34
|
attribute :items, :string, collection: (1..) # At least one item
|
|
33
35
|
attribute :tags, :string, collection: (0..5) # Up to 5 tags
|
|
36
|
+
attribute :name, :string # Default 0..1 (single value)
|
|
34
37
|
----
|
|
35
38
|
|
|
39
|
+
A non-collection attribute given multiple values keeps them. Parsing does not
|
|
40
|
+
raise; the values land in the attribute as an array and `#validate` reports
|
|
41
|
+
`Lutaml::Model::CollectionTrueMissingError`, which `#validate!` raises wrapped
|
|
42
|
+
in a `Lutaml::Model::ValidationError`.
|
|
43
|
+
|
|
44
|
+
[source,ruby]
|
|
45
|
+
----
|
|
46
|
+
obj = Person.from_xml("<person><name>A</name><name>B</name></person>")
|
|
47
|
+
obj.name # => ["A", "B"]
|
|
48
|
+
obj.validate # => [#<Lutaml::Model::CollectionTrueMissingError ...>]
|
|
49
|
+
obj.validate! # raises Lutaml::Model::ValidationError
|
|
50
|
+
----
|
|
51
|
+
|
|
52
|
+
Declare `collection: true` for attributes that hold many values.
|
|
53
|
+
|
|
54
|
+
The rule is about shape, not count. A singular attribute is declared to hold a
|
|
55
|
+
value, not a list, so a one-element array is still a violation:
|
|
56
|
+
|
|
57
|
+
[source,ruby]
|
|
58
|
+
----
|
|
59
|
+
Person.from_json('{"name": ["A"]}').validate
|
|
60
|
+
# => [#<Lutaml::Model::CollectionTrueMissingError ...>]
|
|
61
|
+
----
|
|
62
|
+
|
|
63
|
+
XML has no such distinction — a single `<name>` element is a value, so only a
|
|
64
|
+
repeated element trips the check.
|
|
65
|
+
|
|
66
|
+
An invalid document does not round-trip. While an attribute holds more values
|
|
67
|
+
than it was declared for, serializing it back out will not reproduce the input.
|
|
68
|
+
Fix the violation and it round-trips normally.
|
|
69
|
+
|
|
70
|
+
Two cases still report at parse time rather than on `#validate`:
|
|
71
|
+
|
|
72
|
+
* A declared range that is exceeded raises
|
|
73
|
+
`Lutaml::Model::CollectionCountOutOfRangeError` for key/value formats.
|
|
74
|
+
* A model mapped onto a plain Ruby class with `model` has no `#validate`, so
|
|
75
|
+
there is no later point to report to.
|
|
76
|
+
|
|
77
|
+
A custom deserializer (`with: { from: }`) receives whatever arrived — a single
|
|
78
|
+
node for one occurrence, an array for several, the same shape a custom
|
|
79
|
+
`to:` method already receives for a collection. If that method collapses the
|
|
80
|
+
array to a single value, the over-count is not reported: the method has decided
|
|
81
|
+
what the attribute holds.
|
|
82
|
+
|
|
36
83
|
=== Value enumeration validation
|
|
37
84
|
|
|
38
85
|
The `values` option validates that an attribute value is one of a fixed set of values.
|
|
@@ -163,6 +163,7 @@ require "lutaml/xml/namespace_resolver"
|
|
|
163
163
|
require "lutaml/xml/namespace_scope_config"
|
|
164
164
|
require "lutaml/xml/namespace_type_resolver"
|
|
165
165
|
require "lutaml/xml/namespace_usage"
|
|
166
|
+
require "lutaml/xml/nested_collection_attribute"
|
|
166
167
|
require "lutaml/xml/oga"
|
|
167
168
|
require "lutaml/xml/parse_session"
|
|
168
169
|
require "lutaml/xml/parsed_namespace_declaration"
|
|
@@ -272,7 +272,13 @@ format)
|
|
|
272
272
|
instance)
|
|
273
273
|
end
|
|
274
274
|
|
|
275
|
-
|
|
275
|
+
# Range errors stay eager here. The over-count error only fires when the
|
|
276
|
+
# attribute is not a collection, so this guard defers exactly that case
|
|
277
|
+
# to `.validate` while leaving declared ranges checked at parse. A mapped
|
|
278
|
+
# PORO has no `.validate` to defer into, so it keeps the eager check.
|
|
279
|
+
if attr.collection? || !instance.is_a?(Lutaml::Model::Serialize)
|
|
280
|
+
attr.valid_collection!(value, context)
|
|
281
|
+
end
|
|
276
282
|
rule.deserialize(instance, value, attributes, self)
|
|
277
283
|
end
|
|
278
284
|
|
|
@@ -317,9 +317,31 @@ module Lutaml
|
|
|
317
317
|
end
|
|
318
318
|
|
|
319
319
|
def cast_value(value, register)
|
|
320
|
-
|
|
320
|
+
if list_like?(value)
|
|
321
|
+
build_collection(cast_items(value, register), register: register)
|
|
322
|
+
elsif coerce_to_collection?(value)
|
|
323
|
+
build_collection(cast_items([value], register), register: register)
|
|
324
|
+
else
|
|
325
|
+
cast_element(value, register)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Plain Arrays count as list input. On singular attributes
|
|
330
|
+
# collection_instance? matches Arrays too (their collection class is
|
|
331
|
+
# Array), preserving the Array so strict cardinality can flag it.
|
|
332
|
+
def list_like?(value)
|
|
333
|
+
collection_instance?(value) || (collection? && value.is_a?(::Array))
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Prepare the seed items for a collection. Custom Collection classes
|
|
337
|
+
# cast their own items (idempotently) in #initialize, so hand them a
|
|
338
|
+
# plain array of the existing elements — rebuilding a fresh collection
|
|
339
|
+
# avoids sharing a mutable default across instances. Plain collections
|
|
340
|
+
# cast each element here.
|
|
341
|
+
def cast_items(value, register)
|
|
342
|
+
return value.to_a if custom_collection?
|
|
321
343
|
|
|
322
|
-
|
|
344
|
+
value.map { |v| cast_element(v, register) }
|
|
323
345
|
end
|
|
324
346
|
|
|
325
347
|
# Apply a value map to transform a value.
|
|
@@ -483,10 +505,20 @@ instance_object = nil)
|
|
|
483
505
|
end
|
|
484
506
|
|
|
485
507
|
def valid_value!(value)
|
|
486
|
-
|
|
508
|
+
# nil is never tested against the value set, whether it arrives as the
|
|
509
|
+
# whole value or as one element of a collection (this method recurses
|
|
510
|
+
# per element). valid_pattern! skips nil the same way, and an unset
|
|
511
|
+
# attribute is validate_required!'s business, not the enum's.
|
|
512
|
+
return true if value.nil?
|
|
487
513
|
return true unless enum?
|
|
488
514
|
return true if Utils.uninitialized?(value)
|
|
489
515
|
|
|
516
|
+
# Collection attributes hold multiple values; validate each element
|
|
517
|
+
# against the allowed set rather than the coerced collection itself.
|
|
518
|
+
if value.is_a?(::Array) || collection_instance?(value)
|
|
519
|
+
return value.all? { |v| valid_value!(v) }
|
|
520
|
+
end
|
|
521
|
+
|
|
490
522
|
unless valid_value?(value)
|
|
491
523
|
raise Lutaml::Model::InvalidValueError.new(name, value, enum_values)
|
|
492
524
|
end
|
|
@@ -500,16 +532,17 @@ instance_object = nil)
|
|
|
500
532
|
options[:values].include?(value)
|
|
501
533
|
end
|
|
502
534
|
|
|
503
|
-
# Pattern binds string values only. For a plain :string attribute
|
|
504
|
-
#
|
|
505
|
-
# the :string branch (
|
|
506
|
-
#
|
|
507
|
-
|
|
535
|
+
# Pattern binds actual string values only. For a plain :string attribute
|
|
536
|
+
# every element is a string; for a union the pattern applies to whichever
|
|
537
|
+
# elements took the :string branch (non-string members are exempt). A nil
|
|
538
|
+
# element is skipped along with every other non-string, and Array(value)
|
|
539
|
+
# flattens both plain Arrays and custom Collections so the pattern applies
|
|
540
|
+
# per element instead of to the collection as a whole.
|
|
541
|
+
def valid_pattern!(value, _resolved_type)
|
|
508
542
|
return true unless pattern
|
|
509
543
|
|
|
510
544
|
Array(value).each do |item|
|
|
511
|
-
next unless
|
|
512
|
-
item.is_a?(::String)
|
|
545
|
+
next unless item.is_a?(::String)
|
|
513
546
|
next if pattern.match?(item)
|
|
514
547
|
|
|
515
548
|
raise Lutaml::Model::PatternNotMatchedError.new(name, pattern, item)
|
|
@@ -538,8 +571,11 @@ instance_object = nil)
|
|
|
538
571
|
end
|
|
539
572
|
resolved_type = type(register)
|
|
540
573
|
|
|
541
|
-
|
|
542
|
-
|
|
574
|
+
# Shape before contents. An over-count is a cardinality violation, not
|
|
575
|
+
# an enum or pattern one, and checking it first keeps those predicates
|
|
576
|
+
# from reporting on a value whose shape is already wrong.
|
|
577
|
+
valid_collection!(value, instance_object&.class) &&
|
|
578
|
+
valid_value!(value) &&
|
|
543
579
|
valid_pattern!(value, resolved_type) &&
|
|
544
580
|
validate_polymorphic!(value, resolved_type) &&
|
|
545
581
|
execute_validations!(value)
|
|
@@ -619,7 +655,11 @@ instance_object = nil)
|
|
|
619
655
|
# Allow any value for unbounded collections
|
|
620
656
|
return true if collection == true
|
|
621
657
|
|
|
622
|
-
unless
|
|
658
|
+
unless collection_instance?(value)
|
|
659
|
+
# An absent value satisfies a zero minimum and has nothing to size.
|
|
660
|
+
return true if (value.nil? || Utils.uninitialized?(value)) &&
|
|
661
|
+
resolved_collection.min.zero?
|
|
662
|
+
|
|
623
663
|
raise Lutaml::Model::CollectionCountOutOfRangeError.new(
|
|
624
664
|
name,
|
|
625
665
|
value,
|
|
@@ -627,23 +667,26 @@ instance_object = nil)
|
|
|
627
667
|
)
|
|
628
668
|
end
|
|
629
669
|
|
|
630
|
-
|
|
670
|
+
# cover? rather than between?, so an exclusive range such as (0...5)
|
|
671
|
+
# rejects a count of 5.
|
|
672
|
+
in_range = if resolved_collection.end.infinite?
|
|
673
|
+
value.size >= resolved_collection.begin
|
|
674
|
+
else
|
|
675
|
+
resolved_collection.cover?(value.size)
|
|
676
|
+
end
|
|
631
677
|
|
|
632
|
-
|
|
633
|
-
if value.size < resolved_collection.begin
|
|
634
|
-
raise Lutaml::Model::CollectionCountOutOfRangeError.new(
|
|
635
|
-
name,
|
|
636
|
-
value,
|
|
637
|
-
collection,
|
|
638
|
-
)
|
|
639
|
-
end
|
|
640
|
-
elsif resolved_collection.is_a?(Range) && !resolved_collection.cover?(value.size)
|
|
678
|
+
unless in_range
|
|
641
679
|
raise Lutaml::Model::CollectionCountOutOfRangeError.new(
|
|
642
680
|
name,
|
|
643
681
|
value,
|
|
644
682
|
collection,
|
|
645
683
|
)
|
|
646
684
|
end
|
|
685
|
+
|
|
686
|
+
# Truthy on success: validate_value! chains this with && before the
|
|
687
|
+
# value/pattern/polymorphic/custom checks, so falling through as nil would
|
|
688
|
+
# silently skip them for a valid bounded-range collection.
|
|
689
|
+
true
|
|
647
690
|
end
|
|
648
691
|
|
|
649
692
|
def serialize(value, format, register, options = {})
|
|
@@ -696,7 +739,7 @@ instance_object = nil)
|
|
|
696
739
|
converted: true)
|
|
697
740
|
return build_collection(value.map do |v|
|
|
698
741
|
cast(v, format, register, merged_opts)
|
|
699
|
-
end)
|
|
742
|
+
end, register: register)
|
|
700
743
|
end
|
|
701
744
|
|
|
702
745
|
return value if already_serialized?(resolved_type, value)
|
|
@@ -600,7 +600,7 @@ lutaml_register: Lutaml::Model::Config.default_register)
|
|
|
600
600
|
end
|
|
601
601
|
end
|
|
602
602
|
|
|
603
|
-
#
|
|
603
|
+
# Adds collection-level validations on top of the instance-level ones
|
|
604
604
|
#
|
|
605
605
|
# Collection-level validations run in order and can share state through a
|
|
606
606
|
# context object. Validations can stop the chain early by calling ctx.stop!
|
|
@@ -627,7 +627,10 @@ lutaml_register: Lutaml::Model::Config.default_register)
|
|
|
627
627
|
# end
|
|
628
628
|
# end
|
|
629
629
|
#
|
|
630
|
-
|
|
630
|
+
# Hooks into the guarded pipeline rather than overriding #validate, so
|
|
631
|
+
# the collection rules run inside the cycle guard and a self-referencing
|
|
632
|
+
# collection runs them once rather than twice.
|
|
633
|
+
def collect_validation_errors(register)
|
|
631
634
|
errors = []
|
|
632
635
|
|
|
633
636
|
# Run standard instance-level validations first (inherited from Serializable)
|
|
@@ -7,6 +7,9 @@ module Lutaml
|
|
|
7
7
|
# Provides methods for checking if an attribute is a collection,
|
|
8
8
|
# getting the collection class, building collections, and related operations.
|
|
9
9
|
module CollectionHandler
|
|
10
|
+
# Parameter kinds that carry a named keyword in Method#parameters.
|
|
11
|
+
KEYWORD_PARAM_KINDS = %i[key keyreq].freeze
|
|
12
|
+
|
|
10
13
|
# Get the collection options
|
|
11
14
|
#
|
|
12
15
|
# @return [Object, nil] The collection option value
|
|
@@ -48,10 +51,50 @@ module Lutaml
|
|
|
48
51
|
|
|
49
52
|
# Build a new collection with the given values
|
|
50
53
|
#
|
|
54
|
+
# Custom Collection classes resolve their item type through the register,
|
|
55
|
+
# so thread it through when one is supplied. Plain Array collections and
|
|
56
|
+
# register-less callers keep the prior construction path untouched.
|
|
57
|
+
#
|
|
51
58
|
# @param args [Array] Values to include in the collection
|
|
59
|
+
# @param register [Symbol, nil] Register for custom-collection type resolution
|
|
52
60
|
# @return [Object] A new collection instance
|
|
53
|
-
def build_collection(*args)
|
|
54
|
-
|
|
61
|
+
def build_collection(*args, register: nil)
|
|
62
|
+
items = args.flatten
|
|
63
|
+
unless register && initialize_accepts_register?
|
|
64
|
+
return collection_class.new(items)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
collection_class.new(items, lutaml_register: register)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Whether the collection class can be handed a register at construction.
|
|
71
|
+
#
|
|
72
|
+
# A custom Collection subclass may override #initialize with the
|
|
73
|
+
# positional signature that predates the register, and passing the
|
|
74
|
+
# keyword to one of those raises ArgumentError. Such a subclass resolves
|
|
75
|
+
# its item types against the default register instead — there is no way
|
|
76
|
+
# to reach a constructor that does not accept one.
|
|
77
|
+
#
|
|
78
|
+
# @return [Boolean] true if #initialize declares the register keyword
|
|
79
|
+
def initialize_accepts_register?
|
|
80
|
+
return @initialize_accepts_register if defined?(@initialize_accepts_register)
|
|
81
|
+
|
|
82
|
+
@initialize_accepts_register = custom_collection? &&
|
|
83
|
+
collection_class.instance_method(:initialize).parameters.any? do |kind, param|
|
|
84
|
+
kind == :keyrest ||
|
|
85
|
+
(KEYWORD_PARAM_KINDS.include?(kind) && param == :lutaml_register)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Whether a bare single value assigned to this attribute must be
|
|
90
|
+
# coerced into a one-element collection, so assignment produces the
|
|
91
|
+
# same shape as parsing. nil and the uninitialized sentinel pass
|
|
92
|
+
# through untouched to preserve absent-value semantics.
|
|
93
|
+
#
|
|
94
|
+
# @param value [Object] The value being assigned
|
|
95
|
+
# @return [Boolean] true if the value should be wrapped
|
|
96
|
+
def coerce_to_collection?(value)
|
|
97
|
+
collection? && !value.nil? && !Utils.uninitialized?(value)
|
|
55
98
|
end
|
|
56
99
|
|
|
57
100
|
# Check if this attribute uses a custom collection class
|
|
@@ -81,7 +81,11 @@ module Lutaml
|
|
|
81
81
|
i = instance_variable_get(:"@#{enum_name}") || []
|
|
82
82
|
|
|
83
83
|
if !collection && i.is_a?(Array)
|
|
84
|
-
|
|
84
|
+
# A singular enum stores its one value as a one-element array.
|
|
85
|
+
# Several values is a cardinality violation, so hand the array
|
|
86
|
+
# on instead of collapsing it — #validate reads through here and
|
|
87
|
+
# would otherwise never see the over-count.
|
|
88
|
+
i.size > 1 ? i : i.first
|
|
85
89
|
else
|
|
86
90
|
i.uniq
|
|
87
91
|
end
|
|
@@ -3,19 +3,64 @@
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Model
|
|
5
5
|
module Validation
|
|
6
|
+
# Models being validated on this thread's current stack.
|
|
7
|
+
VALIDATING_KEY = :lutaml_model_validating
|
|
8
|
+
|
|
9
|
+
# Whether a model is already being validated further up this stack.
|
|
10
|
+
def self.visiting?(model)
|
|
11
|
+
stack = Thread.current[VALIDATING_KEY]
|
|
12
|
+
!stack.nil? && stack.key?(model)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# The single guarded entry point. A model can appear inside its own
|
|
16
|
+
# subtree, and re-entering it would recurse forever, so a re-entry
|
|
17
|
+
# returns early — the outer call is already collecting its errors.
|
|
18
|
+
#
|
|
19
|
+
# Override #collect_validation_errors, not this method, to add
|
|
20
|
+
# validation that must run inside the cycle guard. Overriding #validate
|
|
21
|
+
# directly puts the override outside the guard.
|
|
6
22
|
def validate(register: Lutaml::Model::Config.default_register)
|
|
23
|
+
visiting = (Thread.current[VALIDATING_KEY] ||= {}.compare_by_identity)
|
|
24
|
+
return [] if visiting.key?(self)
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
# Marked inside the begin, so an exception delivered between the
|
|
28
|
+
# check and the mark cannot strand an entry on the stack.
|
|
29
|
+
visiting[self] = true
|
|
30
|
+
collect_validation_errors(register)
|
|
31
|
+
ensure
|
|
32
|
+
visiting.delete(self)
|
|
33
|
+
Thread.current[VALIDATING_KEY] = nil if visiting.empty?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def collect_validation_errors(register)
|
|
7
38
|
errors = []
|
|
8
39
|
|
|
9
40
|
self.class.attributes(register).each do |name, attr|
|
|
10
41
|
value = public_send(:"#{name}")
|
|
11
42
|
|
|
12
43
|
begin
|
|
13
|
-
|
|
14
|
-
|
|
44
|
+
# Recurse into nested models — a single child or every element of
|
|
45
|
+
# an array — so their own validation errors surface here. A
|
|
46
|
+
# Collection is itself a model, so it is validated as one: that
|
|
47
|
+
# runs its collection-level rules as well as its elements.
|
|
48
|
+
# #validate takes no arguments here: it is a public override point
|
|
49
|
+
# that downstream models legitimately define with zero arity.
|
|
50
|
+
(value.is_a?(::Array) ? value : [value]).each do |item|
|
|
51
|
+
next unless item.is_a?(Lutaml::Model::Serialize)
|
|
52
|
+
# Skip a model already on the stack rather than calling into it.
|
|
53
|
+
# A downstream #validate override sits above the guard, so
|
|
54
|
+
# re-entering it would run the override's own body a second time.
|
|
55
|
+
next if Validation.visiting?(item)
|
|
56
|
+
|
|
57
|
+
sub_errors = item.validate
|
|
15
58
|
errors.concat(sub_errors) if sub_errors.is_a?(Array)
|
|
16
|
-
else
|
|
17
|
-
attr.validate_value!(value, register, instance_object: self)
|
|
18
59
|
end
|
|
60
|
+
|
|
61
|
+
# Always run attribute-level validation (cardinality, required,
|
|
62
|
+
# enum, pattern, polymorphic, custom) regardless of value type.
|
|
63
|
+
attr.validate_value!(value, register, instance_object: self)
|
|
19
64
|
rescue Lutaml::Model::CollectionCountOutOfRangeError => e
|
|
20
65
|
errors << e unless attr.choice
|
|
21
66
|
rescue Lutaml::Model::InvalidValueError,
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/xml/format.rb
CHANGED
|
@@ -128,6 +128,8 @@ module Lutaml
|
|
|
128
128
|
autoload :TransformationBuilder, "#{__dir__}/transformation_builder"
|
|
129
129
|
autoload :AdapterLoader, "#{__dir__}/adapter_loader"
|
|
130
130
|
autoload :Element, "#{__dir__}/element"
|
|
131
|
+
autoload :NestedCollectionAttribute,
|
|
132
|
+
"#{__dir__}/nested_collection_attribute"
|
|
131
133
|
autoload :ModelTransform, "#{__dir__}/model_transform"
|
|
132
134
|
autoload :ParseSession, "#{__dir__}/parse_session"
|
|
133
135
|
autoload :TypeNamespaceResolver, "#{__dir__}/type_namespace_resolver"
|