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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require_relative "../../../lib/lutaml/model"
|
|
3
|
+
|
|
4
|
+
module CollectionRegisterSpec
|
|
5
|
+
# Inherits Collection#initialize, so it declares the register keyword.
|
|
6
|
+
class Standard < Lutaml::Model::Collection
|
|
7
|
+
instances :items, :string
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Overrides #initialize with the positional signature that predates the
|
|
11
|
+
# register. Passing the keyword to this raises ArgumentError.
|
|
12
|
+
class Positional < Lutaml::Model::Collection
|
|
13
|
+
instances :items, :string
|
|
14
|
+
|
|
15
|
+
def initialize(items = [])
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Accepts and forwards arbitrary keywords, so the register still arrives.
|
|
21
|
+
class Forwarding < Lutaml::Model::Collection
|
|
22
|
+
instances :items, :string
|
|
23
|
+
|
|
24
|
+
def initialize(items = [], **)
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class UsesStandard < Lutaml::Model::Serializable
|
|
30
|
+
attribute :items, :string, collection: Standard
|
|
31
|
+
key_value { map "items", to: :items }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class UsesPositional < Lutaml::Model::Serializable
|
|
35
|
+
attribute :items, :string, collection: Positional
|
|
36
|
+
key_value { map "items", to: :items }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class UsesForwarding < Lutaml::Model::Serializable
|
|
40
|
+
attribute :items, :string, collection: Forwarding
|
|
41
|
+
key_value { map "items", to: :items }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class UsesPlainArray < Lutaml::Model::Serializable
|
|
45
|
+
attribute :items, :string, collection: true
|
|
46
|
+
key_value { map "items", to: :items }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
RSpec.describe "collection construction and the register" do
|
|
51
|
+
let(:register) { Lutaml::Model::Register.new(:collection_register_spec) }
|
|
52
|
+
|
|
53
|
+
before { Lutaml::Model::GlobalRegister.register(register) }
|
|
54
|
+
|
|
55
|
+
describe "a custom collection whose constructor takes the register" do
|
|
56
|
+
it "receives the register used for the parse" do
|
|
57
|
+
obj = CollectionRegisterSpec::UsesStandard.from_json(
|
|
58
|
+
'{"items":["x","y"]}',
|
|
59
|
+
register: :collection_register_spec,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
expect(obj.items.lutaml_register).to eq(:collection_register_spec)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "receives it through a forwarding constructor too" do
|
|
66
|
+
obj = CollectionRegisterSpec::UsesForwarding.from_json(
|
|
67
|
+
'{"items":["x","y"]}',
|
|
68
|
+
register: :collection_register_spec,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
expect(obj.items.lutaml_register).to eq(:collection_register_spec)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe "a custom collection with a positional-only constructor" do
|
|
76
|
+
it "parses without raising ArgumentError" do
|
|
77
|
+
obj = nil
|
|
78
|
+
|
|
79
|
+
expect do
|
|
80
|
+
obj = CollectionRegisterSpec::UsesPositional.from_json(
|
|
81
|
+
'{"items":["x","y"]}',
|
|
82
|
+
)
|
|
83
|
+
end.not_to raise_error
|
|
84
|
+
|
|
85
|
+
expect(obj.items.to_a).to eq(%w[x y])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "accepts assignment without raising ArgumentError" do
|
|
89
|
+
obj = CollectionRegisterSpec::UsesPositional.new
|
|
90
|
+
|
|
91
|
+
expect { obj.items = %w[x y] }.not_to raise_error
|
|
92
|
+
expect(obj.items.to_a).to eq(%w[x y])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# There is no way to hand a register to a constructor that does not take
|
|
96
|
+
# one, so such a collection resolves against the default register.
|
|
97
|
+
it "falls back to the default register under a non-default parse" do
|
|
98
|
+
obj = CollectionRegisterSpec::UsesPositional.from_json(
|
|
99
|
+
'{"items":["x"]}',
|
|
100
|
+
register: :collection_register_spec,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
expect(obj.items.lutaml_register)
|
|
104
|
+
.to eq(Lutaml::Model::Config.default_register)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe "a plain collection: true attribute" do
|
|
109
|
+
it "still builds a bare Array when a register is in play" do
|
|
110
|
+
obj = CollectionRegisterSpec::UsesPlainArray.from_json(
|
|
111
|
+
'{"items":["x","y"]}',
|
|
112
|
+
register: :collection_register_spec,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
expect(obj.items).to be_an(Array)
|
|
116
|
+
expect(obj.items).to eq(%w[x y])
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require_relative "../../../lib/lutaml/model"
|
|
3
|
+
|
|
4
|
+
module CollectionSingleValueCoercion
|
|
5
|
+
class Item < Lutaml::Model::Serializable
|
|
6
|
+
attribute :name, :string
|
|
7
|
+
|
|
8
|
+
xml do
|
|
9
|
+
element "item"
|
|
10
|
+
map_element "name", to: :name
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Basket < Lutaml::Model::Serializable
|
|
15
|
+
attribute :items, Item, collection: true
|
|
16
|
+
attribute :labels, :string, collection: true
|
|
17
|
+
attribute :sizes, :string, collection: 1..2
|
|
18
|
+
|
|
19
|
+
xml do
|
|
20
|
+
element "basket"
|
|
21
|
+
map_element "item", to: :items
|
|
22
|
+
map_element "label", to: :labels
|
|
23
|
+
map_element "size", to: :sizes
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class NameParts < Lutaml::Model::Collection
|
|
28
|
+
instances :parts, :string
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class MetadataEntries < Lutaml::Model::Collection
|
|
32
|
+
instances :entries, :hash
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class Person < Lutaml::Model::Serializable
|
|
36
|
+
attribute :name_parts, :string, collection: NameParts
|
|
37
|
+
attribute :metadata_entries, :hash, collection: MetadataEntries
|
|
38
|
+
|
|
39
|
+
xml do
|
|
40
|
+
element "person"
|
|
41
|
+
map_element "part", to: :name_parts
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class Coded < Lutaml::Model::Serializable
|
|
46
|
+
attribute :codes, :string, collection: true, pattern: /\A[a-z]+\z/
|
|
47
|
+
|
|
48
|
+
xml do
|
|
49
|
+
element "coded"
|
|
50
|
+
map_element "code", to: :codes
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Pattern applied to an attribute backed by a custom Collection class. The
|
|
55
|
+
# value is a Collection instance (not a plain Array), so pattern validation
|
|
56
|
+
# must flatten it per element rather than treat it as a single non-string.
|
|
57
|
+
class CodedCustom < Lutaml::Model::Serializable
|
|
58
|
+
attribute :codes, :string, collection: NameParts, pattern: /\A[a-z]+\z/
|
|
59
|
+
|
|
60
|
+
xml do
|
|
61
|
+
element "coded_custom"
|
|
62
|
+
map_element "code", to: :codes
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
RSpec.describe "single value coercion on collection attributes" do
|
|
68
|
+
describe "setter assignment" do
|
|
69
|
+
it "wraps a single model instance into a one-element array" do
|
|
70
|
+
basket = CollectionSingleValueCoercion::Basket.new
|
|
71
|
+
basket.items = CollectionSingleValueCoercion::Item.new(name: "one")
|
|
72
|
+
|
|
73
|
+
expect(basket.items).to be_a(Array)
|
|
74
|
+
expect(basket.items.map(&:name)).to eq(["one"])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "wraps a single primitive into a one-element array" do
|
|
78
|
+
basket = CollectionSingleValueCoercion::Basket.new
|
|
79
|
+
basket.labels = "fragile"
|
|
80
|
+
|
|
81
|
+
expect(basket.labels).to eq(["fragile"])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "leaves nil untouched" do
|
|
85
|
+
basket = CollectionSingleValueCoercion::Basket.new
|
|
86
|
+
basket.labels = nil
|
|
87
|
+
|
|
88
|
+
expect(basket.labels).to be_nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "leaves array assignment unchanged" do
|
|
92
|
+
basket = CollectionSingleValueCoercion::Basket.new
|
|
93
|
+
basket.labels = %w[a b]
|
|
94
|
+
|
|
95
|
+
expect(basket.labels).to eq(%w[a b])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe "constructor assignment" do
|
|
100
|
+
it "wraps a single value passed to the constructor" do
|
|
101
|
+
basket = CollectionSingleValueCoercion::Basket.new(labels: "solo")
|
|
102
|
+
|
|
103
|
+
expect(basket.labels).to eq(["solo"])
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "produces the same shape as parsing one element" do
|
|
107
|
+
parsed = CollectionSingleValueCoercion::Basket.from_xml(
|
|
108
|
+
"<basket><label>solo</label></basket>",
|
|
109
|
+
)
|
|
110
|
+
built = CollectionSingleValueCoercion::Basket.new(labels: "solo")
|
|
111
|
+
|
|
112
|
+
expect(built.labels).to eq(parsed.labels)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
describe "bounded collections" do
|
|
117
|
+
it "wraps a single value and satisfies the range validation" do
|
|
118
|
+
basket = CollectionSingleValueCoercion::Basket.new(sizes: "small")
|
|
119
|
+
|
|
120
|
+
expect(basket.sizes).to eq(["small"])
|
|
121
|
+
expect { basket.validate! }.not_to raise_error
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe "custom collection classes" do
|
|
126
|
+
it "wraps a single value into the custom collection" do
|
|
127
|
+
person = CollectionSingleValueCoercion::Person.new
|
|
128
|
+
person.name_parts = "Ada"
|
|
129
|
+
|
|
130
|
+
expect(person.name_parts)
|
|
131
|
+
.to be_a(CollectionSingleValueCoercion::NameParts)
|
|
132
|
+
expect(person.name_parts.to_a.size).to eq(1)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "routes a plain Array through the collection branch element-wise" do
|
|
136
|
+
person = CollectionSingleValueCoercion::Person.new
|
|
137
|
+
person.name_parts = %w[Ada Lovelace]
|
|
138
|
+
|
|
139
|
+
expect(person.name_parts)
|
|
140
|
+
.to be_a(CollectionSingleValueCoercion::NameParts)
|
|
141
|
+
expect(person.name_parts.to_a.size).to eq(2)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "does not double-cast arrays for hash-valued custom collections" do
|
|
145
|
+
person = CollectionSingleValueCoercion::Person.new
|
|
146
|
+
person.metadata_entries = [{ "text" => "x" }]
|
|
147
|
+
|
|
148
|
+
expect(person.metadata_entries)
|
|
149
|
+
.to be_a(CollectionSingleValueCoercion::MetadataEntries)
|
|
150
|
+
expect(person.metadata_entries.to_a).to eq(["x"])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "does not double-cast single values for hash-valued custom collections" do
|
|
154
|
+
person = CollectionSingleValueCoercion::Person.new
|
|
155
|
+
person.metadata_entries = { "text" => "x" }
|
|
156
|
+
|
|
157
|
+
expect(person.metadata_entries)
|
|
158
|
+
.to be_a(CollectionSingleValueCoercion::MetadataEntries)
|
|
159
|
+
expect(person.metadata_entries.to_a).to eq(["x"])
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "rebuilds a fresh collection instead of sharing the assigned one" do
|
|
163
|
+
shared = CollectionSingleValueCoercion::NameParts.new(%w[Ada])
|
|
164
|
+
a = CollectionSingleValueCoercion::Person.new
|
|
165
|
+
b = CollectionSingleValueCoercion::Person.new
|
|
166
|
+
|
|
167
|
+
a.name_parts = shared
|
|
168
|
+
b.name_parts = shared
|
|
169
|
+
|
|
170
|
+
expect(a.name_parts).not_to be(shared)
|
|
171
|
+
expect(a.name_parts).not_to be(b.name_parts)
|
|
172
|
+
expect(a.name_parts.to_a).to eq(%w[Ada])
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
describe "pattern validation on collection attributes" do
|
|
177
|
+
it "validates a coerced single value per element" do
|
|
178
|
+
coded = CollectionSingleValueCoercion::Coded.new(codes: "abc")
|
|
179
|
+
|
|
180
|
+
expect(coded.codes).to eq(["abc"])
|
|
181
|
+
expect { coded.validate! }.not_to raise_error
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "validates each element of an assigned array" do
|
|
185
|
+
coded = CollectionSingleValueCoercion::Coded.new(codes: %w[abc def])
|
|
186
|
+
|
|
187
|
+
expect { coded.validate! }.not_to raise_error
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it "rejects an element that violates the pattern" do
|
|
191
|
+
coded = CollectionSingleValueCoercion::Coded.new(codes: %w[abc DEF])
|
|
192
|
+
|
|
193
|
+
expect { coded.validate! }
|
|
194
|
+
.to raise_error(Lutaml::Model::ValidationError, /DEF/)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "validates each element of a custom Collection per element" do
|
|
198
|
+
coded = CollectionSingleValueCoercion::CodedCustom.new(codes: %w[abc def])
|
|
199
|
+
|
|
200
|
+
expect { coded.validate! }.not_to raise_error
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it "rejects a bad element inside a custom Collection" do
|
|
204
|
+
coded = CollectionSingleValueCoercion::CodedCustom.new(codes: %w[abc DEF])
|
|
205
|
+
|
|
206
|
+
expect { coded.validate! }
|
|
207
|
+
.to raise_error(Lutaml::Model::ValidationError, /DEF/)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
describe "serialization round-trip" do
|
|
212
|
+
it "serializes a coerced single value as a repeated element would" do
|
|
213
|
+
basket = CollectionSingleValueCoercion::Basket.new
|
|
214
|
+
basket.items = CollectionSingleValueCoercion::Item.new(name: "one")
|
|
215
|
+
|
|
216
|
+
round_tripped = CollectionSingleValueCoercion::Basket.from_xml(
|
|
217
|
+
basket.to_xml,
|
|
218
|
+
)
|
|
219
|
+
expect(round_tripped.items.map(&:name)).to eq(["one"])
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -280,14 +280,17 @@ RSpec.describe "RootMapping" do
|
|
|
280
280
|
}
|
|
281
281
|
end
|
|
282
282
|
|
|
283
|
-
it "
|
|
283
|
+
it "parses, then reports the missing `collection: true` on validate" do
|
|
284
|
+
obj = nil
|
|
284
285
|
expect do
|
|
285
|
-
RootMappingSpec::CeramicCollectionWithoutCollectionTrue
|
|
286
|
-
:"from_#{format}", input
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
286
|
+
obj = RootMappingSpec::CeramicCollectionWithoutCollectionTrue
|
|
287
|
+
.public_send(:"from_#{format}", input)
|
|
288
|
+
end.not_to raise_error
|
|
289
|
+
|
|
290
|
+
expect(obj.validate).to include(
|
|
291
|
+
an_object_having_attributes(
|
|
292
|
+
message: "May be `collection: true` is missing for `ceramics` in RootMappingSpec::CeramicCollectionWithoutCollectionTrue",
|
|
293
|
+
),
|
|
291
294
|
)
|
|
292
295
|
end
|
|
293
296
|
end
|