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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "spec_helper"
|
|
2
2
|
require "lutaml/model/schema"
|
|
3
|
+
require "nokogiri"
|
|
3
4
|
|
|
4
5
|
module SchemaGeneration
|
|
5
6
|
class Glaze < Lutaml::Model::Serializable
|
|
@@ -22,34 +23,793 @@ RSpec.describe Lutaml::Xml::Schema::XsdSchema do
|
|
|
22
23
|
|
|
23
24
|
expected_schema = <<~XSD
|
|
24
25
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
25
|
-
<schema xmlns="http://www.w3.org/2001/XMLSchema">
|
|
26
|
-
<element name="SchemaGeneration::Vase">
|
|
27
|
-
<complexType>
|
|
28
|
-
<sequence>
|
|
29
|
-
<element name="height" type="xs:float"/>
|
|
30
|
-
<element name="diameter" type="xs:float"/>
|
|
31
|
-
<element name="glaze">
|
|
32
|
-
<complexType>
|
|
33
|
-
<sequence>
|
|
34
|
-
<element name="color" type="xs:string"/>
|
|
35
|
-
<element name="finish" type="xs:string"/>
|
|
36
|
-
</sequence>
|
|
37
|
-
</complexType>
|
|
38
|
-
</element>
|
|
39
|
-
<element name="materials" minOccurs="0" maxOccurs="unbounded">
|
|
40
|
-
<complexType>
|
|
41
|
-
<sequence>
|
|
42
|
-
<element name="item" type="xs:string"/>
|
|
43
|
-
</sequence>
|
|
44
|
-
</complexType>
|
|
45
|
-
</element>
|
|
46
|
-
</sequence>
|
|
47
|
-
</complexType>
|
|
48
|
-
</element>
|
|
49
|
-
</schema>
|
|
26
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
27
|
+
<xs:element name="SchemaGeneration::Vase">
|
|
28
|
+
<xs:complexType>
|
|
29
|
+
<xs:sequence>
|
|
30
|
+
<xs:element name="height" type="xs:float"/>
|
|
31
|
+
<xs:element name="diameter" type="xs:float"/>
|
|
32
|
+
<xs:element name="glaze">
|
|
33
|
+
<xs:complexType>
|
|
34
|
+
<xs:sequence>
|
|
35
|
+
<xs:element name="color" type="xs:string"/>
|
|
36
|
+
<xs:element name="finish" type="xs:string"/>
|
|
37
|
+
</xs:sequence>
|
|
38
|
+
</xs:complexType>
|
|
39
|
+
</xs:element>
|
|
40
|
+
<xs:element name="materials" minOccurs="0" maxOccurs="unbounded">
|
|
41
|
+
<xs:complexType>
|
|
42
|
+
<xs:sequence>
|
|
43
|
+
<xs:element name="item" type="xs:string"/>
|
|
44
|
+
</xs:sequence>
|
|
45
|
+
</xs:complexType>
|
|
46
|
+
</xs:element>
|
|
47
|
+
</xs:sequence>
|
|
48
|
+
</xs:complexType>
|
|
49
|
+
</xs:element>
|
|
50
|
+
</xs:schema>
|
|
50
51
|
XSD
|
|
51
52
|
|
|
52
53
|
expect(schema).to eq(expected_schema)
|
|
53
54
|
end
|
|
54
55
|
end
|
|
56
|
+
|
|
57
|
+
# Regression guard for issue #717: the generated XSD bound the XSD namespace
|
|
58
|
+
# only as the default xmlns while referencing built-in types with an
|
|
59
|
+
# undeclared "xs:" prefix, so no XSD processor could load the output.
|
|
60
|
+
describe "generated XSD validity (issue #717)" do
|
|
61
|
+
it "produces a schema Nokogiri can load (no target namespace)" do
|
|
62
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
63
|
+
attribute :id, :string
|
|
64
|
+
xml do
|
|
65
|
+
root "address"
|
|
66
|
+
map_element "id", to: :id
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
xsd = Lutaml::Model::Schema.to_xsd(klass)
|
|
71
|
+
|
|
72
|
+
expect(xsd).to include('<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"')
|
|
73
|
+
expect(xsd).to include('type="xs:string"')
|
|
74
|
+
expect { Nokogiri::XML::Schema(xsd) }.not_to raise_error
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "produces a schema Nokogiri can load (with target namespace)" do
|
|
78
|
+
ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
79
|
+
uri "http://example.com/addr"
|
|
80
|
+
prefix_default "ex"
|
|
81
|
+
element_form_default :qualified
|
|
82
|
+
end
|
|
83
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
84
|
+
attribute :street, :string
|
|
85
|
+
xml do
|
|
86
|
+
root "address"
|
|
87
|
+
namespace ns
|
|
88
|
+
map_element "street", to: :street
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
xsd = Lutaml::Model::Schema.to_xsd(klass)
|
|
93
|
+
|
|
94
|
+
expect(xsd).to include('xmlns:xs="http://www.w3.org/2001/XMLSchema"')
|
|
95
|
+
expect(xsd).to include('targetNamespace="http://example.com/addr"')
|
|
96
|
+
expect { Nokogiri::XML::Schema(xsd) }.not_to raise_error
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The hole #717 fell through: a nested named-type reference under a target
|
|
100
|
+
# namespace must be prefixed, or it resolves to no-namespace and fails.
|
|
101
|
+
it "qualifies named-type references under a target namespace" do
|
|
102
|
+
ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
103
|
+
uri "http://example.com/addr"
|
|
104
|
+
prefix_default "ex"
|
|
105
|
+
element_form_default :qualified
|
|
106
|
+
end
|
|
107
|
+
widget = Class.new(Lutaml::Model::Serializable) do
|
|
108
|
+
attribute :label, :string
|
|
109
|
+
xml do
|
|
110
|
+
root "widget"
|
|
111
|
+
type_name "WidgetType"
|
|
112
|
+
map_element "label", to: :label
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
doc = Class.new(Lutaml::Model::Serializable) do
|
|
116
|
+
attribute :widget, widget
|
|
117
|
+
attribute :title, :string
|
|
118
|
+
xml do
|
|
119
|
+
root "doc"
|
|
120
|
+
namespace ns
|
|
121
|
+
map_element "widget", to: :widget
|
|
122
|
+
map_element "title", to: :title
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
xsd = Lutaml::Model::Schema.to_xsd(doc)
|
|
127
|
+
|
|
128
|
+
expect(xsd).to include('xmlns:ex="http://example.com/addr"')
|
|
129
|
+
expect(xsd).to include('type="ex:WidgetType"')
|
|
130
|
+
expect(xsd).to include('<xs:complexType name="WidgetType">')
|
|
131
|
+
expect(xsd).not_to include("<xs:import")
|
|
132
|
+
expect { Nokogiri::XML::Schema(xsd) }.not_to raise_error
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Guard against over-eager prefixing: with no namespace, named-type refs
|
|
136
|
+
# must stay unqualified (they resolve to no-namespace, which is valid).
|
|
137
|
+
it "leaves named-type references unqualified when there is no namespace" do
|
|
138
|
+
inner = Class.new(Lutaml::Model::Serializable) do
|
|
139
|
+
attribute :city, :string
|
|
140
|
+
xml do
|
|
141
|
+
root "address"
|
|
142
|
+
type_name "AddressType"
|
|
143
|
+
map_element "city", to: :city
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
outer = Class.new(Lutaml::Model::Serializable) do
|
|
147
|
+
attribute :address, inner
|
|
148
|
+
xml do
|
|
149
|
+
root "person"
|
|
150
|
+
map_element "address", to: :address
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
xsd = Lutaml::Model::Schema.to_xsd(outer)
|
|
155
|
+
|
|
156
|
+
expect(xsd).to include('type="AddressType"')
|
|
157
|
+
expect(xsd).not_to include('type=":AddressType"')
|
|
158
|
+
expect { Nokogiri::XML::Schema(xsd) }.not_to raise_error
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "qualifies named-type references for collections under a target namespace" do
|
|
162
|
+
ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
163
|
+
uri "http://example.com/addr"
|
|
164
|
+
prefix_default "ex"
|
|
165
|
+
element_form_default :qualified
|
|
166
|
+
end
|
|
167
|
+
item = Class.new(Lutaml::Model::Serializable) do
|
|
168
|
+
attribute :label, :string
|
|
169
|
+
xml do
|
|
170
|
+
root "item"
|
|
171
|
+
type_name "ItemType"
|
|
172
|
+
map_element "label", to: :label
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
doc = Class.new(Lutaml::Model::Serializable) do
|
|
176
|
+
attribute :items, item, collection: true
|
|
177
|
+
xml do
|
|
178
|
+
root "doc"
|
|
179
|
+
namespace ns
|
|
180
|
+
map_element "item", to: :items
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
xsd = Lutaml::Model::Schema.to_xsd(doc)
|
|
185
|
+
|
|
186
|
+
expect(xsd).to include('type="ex:ItemType"')
|
|
187
|
+
expect { Nokogiri::XML::Schema(xsd) }.not_to raise_error
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# A nested model in its OWN (different) namespace is imported and referenced
|
|
191
|
+
# by its own prefix, NOT folded into the root namespace. (A schema document
|
|
192
|
+
# has a single targetNamespace, so its type lives in its own schema.)
|
|
193
|
+
it "imports and prefixes a nested model that declares a different namespace" do
|
|
194
|
+
w_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
195
|
+
uri "http://w.example/w"
|
|
196
|
+
prefix_default "w"
|
|
197
|
+
element_form_default :qualified
|
|
198
|
+
schema_location "widget.xsd"
|
|
199
|
+
end
|
|
200
|
+
ex_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
201
|
+
uri "http://ex.example/ex"
|
|
202
|
+
prefix_default "ex"
|
|
203
|
+
element_form_default :qualified
|
|
204
|
+
end
|
|
205
|
+
widget = Class.new(Lutaml::Model::Serializable) do
|
|
206
|
+
attribute :label, :string
|
|
207
|
+
xml do
|
|
208
|
+
root "widget"
|
|
209
|
+
namespace w_ns
|
|
210
|
+
type_name "WidgetType"
|
|
211
|
+
map_element "label", to: :label
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
doc = Class.new(Lutaml::Model::Serializable) do
|
|
215
|
+
attribute :widget, widget
|
|
216
|
+
xml do
|
|
217
|
+
root "doc"
|
|
218
|
+
namespace ex_ns
|
|
219
|
+
map_element "widget", to: :widget
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
xsd = Lutaml::Model::Schema.to_xsd(doc)
|
|
224
|
+
|
|
225
|
+
# both namespaces declared, foreign one imported, ref uses its own prefix
|
|
226
|
+
expect(xsd).to include('xmlns:w="http://w.example/w"')
|
|
227
|
+
expect(xsd).to include('targetNamespace="http://ex.example/ex"')
|
|
228
|
+
expect(xsd).to include('<xs:import namespace="http://w.example/w" schemaLocation="widget.xsd"/>')
|
|
229
|
+
expect(xsd).to include('type="w:WidgetType"')
|
|
230
|
+
# the foreign type is NOT defined in this document
|
|
231
|
+
expect(xsd).not_to include('<xs:complexType name="WidgetType">')
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# A target namespace whose class declares no prefix_default still needs a
|
|
235
|
+
# usable prefix for named-type QNames; one is synthesised (tns).
|
|
236
|
+
it "synthesises a prefix for a target namespace with no prefix_default" do
|
|
237
|
+
ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
238
|
+
uri "http://example.com/np"
|
|
239
|
+
element_form_default :qualified
|
|
240
|
+
end
|
|
241
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
242
|
+
attribute :id, :string
|
|
243
|
+
xml do
|
|
244
|
+
root "root"
|
|
245
|
+
namespace ns
|
|
246
|
+
type_name "RootType"
|
|
247
|
+
map_element "id", to: :id
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
xsd = Lutaml::Model::Schema.to_xsd(klass)
|
|
252
|
+
|
|
253
|
+
expect(xsd).to include('xmlns:tns="http://example.com/np"')
|
|
254
|
+
expect(xsd).to include('type="tns:RootType"')
|
|
255
|
+
expect { Nokogiri::XML::Schema(xsd) }.not_to raise_error
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Two different namespaces bound to the same prefix are unresolvable; the
|
|
259
|
+
# generator raises rather than emit a silently-wrong QName.
|
|
260
|
+
it "raises when two namespaces share a prefix" do
|
|
261
|
+
a_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
262
|
+
uri "http://a.example"
|
|
263
|
+
prefix_default "p"
|
|
264
|
+
end
|
|
265
|
+
b_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
266
|
+
uri "http://b.example"
|
|
267
|
+
prefix_default "p"
|
|
268
|
+
end
|
|
269
|
+
child = Class.new(Lutaml::Model::Serializable) do
|
|
270
|
+
attribute :v, :string
|
|
271
|
+
xml do
|
|
272
|
+
root "child"
|
|
273
|
+
namespace b_ns
|
|
274
|
+
type_name "BType"
|
|
275
|
+
map_element "v", to: :v
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
parent = Class.new(Lutaml::Model::Serializable) do
|
|
279
|
+
attribute :child, child
|
|
280
|
+
xml do
|
|
281
|
+
root "parent"
|
|
282
|
+
namespace a_ns
|
|
283
|
+
map_element "child", to: :child
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
expect { Lutaml::Model::Schema.to_xsd(parent) }
|
|
288
|
+
.to raise_error(Lutaml::Model::Error, /prefix 'p' is bound to two/)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# A recursive model (A -> B -> A) must terminate and define both types.
|
|
292
|
+
it "handles recursive model references without overflowing" do
|
|
293
|
+
rec_a = Class.new(Lutaml::Model::Serializable)
|
|
294
|
+
rec_b = Class.new(Lutaml::Model::Serializable) do
|
|
295
|
+
attribute :a, rec_a
|
|
296
|
+
xml do
|
|
297
|
+
root "b"
|
|
298
|
+
type_name "BType"
|
|
299
|
+
map_element "a", to: :a
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
rec_a.class_eval do
|
|
303
|
+
attribute :b, rec_b
|
|
304
|
+
xml do
|
|
305
|
+
root "a"
|
|
306
|
+
type_name "AType"
|
|
307
|
+
map_element "b", to: :b
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
xsd = nil
|
|
312
|
+
expect { xsd = Lutaml::Model::Schema.to_xsd(rec_a) }.not_to raise_error
|
|
313
|
+
expect(xsd).to include('<xs:complexType name="AType">')
|
|
314
|
+
expect(xsd).to include('<xs:complexType name="BType">')
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# A foreign namespace with no usable prefix (nil, empty, or the reserved
|
|
318
|
+
# "xs") cannot be referenced by a prefixed QName and must not borrow the
|
|
319
|
+
# target prefix — raise instead.
|
|
320
|
+
[nil, "", "xs"].each do |bad_prefix|
|
|
321
|
+
it "raises for a foreign namespace with unusable prefix #{bad_prefix.inspect}" do
|
|
322
|
+
foreign = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
323
|
+
uri "http://foreign.example"
|
|
324
|
+
prefix_default bad_prefix unless bad_prefix.nil?
|
|
325
|
+
element_form_default :qualified
|
|
326
|
+
end
|
|
327
|
+
ex_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
328
|
+
uri "http://ex.example/ex"
|
|
329
|
+
prefix_default "ex"
|
|
330
|
+
end
|
|
331
|
+
child = Class.new(Lutaml::Model::Serializable) do
|
|
332
|
+
attribute :v, :string
|
|
333
|
+
xml do
|
|
334
|
+
root "child"
|
|
335
|
+
namespace foreign
|
|
336
|
+
type_name "ForeignType"
|
|
337
|
+
map_element "v", to: :v
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
parent = Class.new(Lutaml::Model::Serializable) do
|
|
341
|
+
attribute :child, child
|
|
342
|
+
xml do
|
|
343
|
+
root "parent"
|
|
344
|
+
namespace ex_ns
|
|
345
|
+
map_element "child", to: :child
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
expect { Lutaml::Model::Schema.to_xsd(parent) }
|
|
350
|
+
.to raise_error(Lutaml::Model::Error, /foreign namespace .* usable prefix/)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Namespaces used only INSIDE an imported foreign type belong to that
|
|
355
|
+
# type's own schema document — they must not be declared, imported, or
|
|
356
|
+
# validated here.
|
|
357
|
+
it "ignores namespaces nested inside an imported foreign subtree" do
|
|
358
|
+
inner_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
359
|
+
uri "http://c.example" # deliberately no prefix_default
|
|
360
|
+
end
|
|
361
|
+
w_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
362
|
+
uri "http://w.example/w"
|
|
363
|
+
prefix_default "w"
|
|
364
|
+
schema_location "widget.xsd"
|
|
365
|
+
end
|
|
366
|
+
ex_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
367
|
+
uri "http://ex.example/ex"
|
|
368
|
+
prefix_default "ex"
|
|
369
|
+
end
|
|
370
|
+
inner = Class.new(Lutaml::Model::Serializable) do
|
|
371
|
+
attribute :v, :string
|
|
372
|
+
xml do
|
|
373
|
+
root "inner"
|
|
374
|
+
namespace inner_ns
|
|
375
|
+
type_name "InnerType"
|
|
376
|
+
map_element "v", to: :v
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
widget = Class.new(Lutaml::Model::Serializable) do
|
|
380
|
+
attribute :inner, inner
|
|
381
|
+
xml do
|
|
382
|
+
root "widget"
|
|
383
|
+
namespace w_ns
|
|
384
|
+
type_name "WidgetType"
|
|
385
|
+
map_element "inner", to: :inner
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
doc = Class.new(Lutaml::Model::Serializable) do
|
|
389
|
+
attribute :widget, widget
|
|
390
|
+
xml do
|
|
391
|
+
root "doc"
|
|
392
|
+
namespace ex_ns
|
|
393
|
+
map_element "widget", to: :widget
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
xsd = Lutaml::Model::Schema.to_xsd(doc)
|
|
398
|
+
|
|
399
|
+
expect(xsd).to include('type="w:WidgetType"')
|
|
400
|
+
expect(xsd).not_to include("http://c.example")
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
it "raises when a foreign namespace has no schema_location" do
|
|
404
|
+
w_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
405
|
+
uri "http://w-noloc.example/w"
|
|
406
|
+
prefix_default "w" # no schema_location
|
|
407
|
+
end
|
|
408
|
+
ex_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
409
|
+
uri "http://ex.example/ex"
|
|
410
|
+
prefix_default "ex"
|
|
411
|
+
end
|
|
412
|
+
widget = Class.new(Lutaml::Model::Serializable) do
|
|
413
|
+
attribute :label, :string
|
|
414
|
+
xml do
|
|
415
|
+
root "widget"
|
|
416
|
+
namespace w_ns
|
|
417
|
+
type_name "WidgetType"
|
|
418
|
+
map_element "label", to: :label
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
doc = Class.new(Lutaml::Model::Serializable) do
|
|
422
|
+
attribute :widget, widget
|
|
423
|
+
xml do
|
|
424
|
+
root "doc"
|
|
425
|
+
namespace ex_ns
|
|
426
|
+
map_element "widget", to: :widget
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
expect { Lutaml::Model::Schema.to_xsd(doc) }
|
|
431
|
+
.to raise_error(Lutaml::Model::Error, /schema_location/)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
it "downgrades foreign-namespace errors to warnings under skip_validation" do
|
|
435
|
+
w_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
436
|
+
uri "http://w-warn.example/w"
|
|
437
|
+
prefix_default "w" # no schema_location -> error without skip_validation
|
|
438
|
+
end
|
|
439
|
+
ex_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
440
|
+
uri "http://ex.example/ex"
|
|
441
|
+
prefix_default "ex"
|
|
442
|
+
end
|
|
443
|
+
widget = Class.new(Lutaml::Model::Serializable) do
|
|
444
|
+
attribute :label, :string
|
|
445
|
+
xml do
|
|
446
|
+
root "widget"
|
|
447
|
+
namespace w_ns
|
|
448
|
+
type_name "WidgetType"
|
|
449
|
+
map_element "label", to: :label
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
doc = Class.new(Lutaml::Model::Serializable) do
|
|
453
|
+
attribute :widget, widget
|
|
454
|
+
xml do
|
|
455
|
+
root "doc"
|
|
456
|
+
namespace ex_ns
|
|
457
|
+
map_element "widget", to: :widget
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
xsd = nil
|
|
462
|
+
expect { xsd = Lutaml::Model::Schema.to_xsd(doc, skip_validation: true) }
|
|
463
|
+
.to output(/schema_location/).to_stderr
|
|
464
|
+
|
|
465
|
+
# Best-effort output still declares the usable prefix its QNames use,
|
|
466
|
+
# and imports the namespace (location-less).
|
|
467
|
+
expect(xsd).to include('xmlns:w="http://w-warn.example/w"')
|
|
468
|
+
expect(xsd).to include('type="w:WidgetType"')
|
|
469
|
+
expect(xsd).to include('<xs:import namespace="http://w-warn.example/w"/>')
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Prefix errors emit QNames that resolve to the WRONG namespace, so they
|
|
473
|
+
# stay hard errors even under skip_validation.
|
|
474
|
+
it "does not downgrade prefix errors under skip_validation" do
|
|
475
|
+
no_pfx = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
476
|
+
uri "http://p-hard.example" # no prefix_default
|
|
477
|
+
end
|
|
478
|
+
ex_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
479
|
+
uri "http://ex.example/ex"
|
|
480
|
+
prefix_default "ex"
|
|
481
|
+
end
|
|
482
|
+
child = Class.new(Lutaml::Model::Serializable) do
|
|
483
|
+
attribute :v, :string
|
|
484
|
+
xml do
|
|
485
|
+
root "child"
|
|
486
|
+
namespace no_pfx
|
|
487
|
+
type_name "PType"
|
|
488
|
+
map_element "v", to: :v
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
parent = Class.new(Lutaml::Model::Serializable) do
|
|
492
|
+
attribute :child, child
|
|
493
|
+
xml do
|
|
494
|
+
root "parent"
|
|
495
|
+
namespace ex_ns
|
|
496
|
+
map_element "child", to: :child
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
expect do
|
|
501
|
+
Lutaml::Model::Schema.to_xsd(parent, skip_validation: true)
|
|
502
|
+
end.to raise_error(Lutaml::Model::Error, /usable prefix/)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# A Type::Value attribute type that declares its own namespace resolves
|
|
506
|
+
# through that namespace's imported schema.
|
|
507
|
+
it "declares and imports a Type::Value attribute type's namespace" do
|
|
508
|
+
gml_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
509
|
+
uri "http://www.opengis.net/gml"
|
|
510
|
+
prefix_default "gml"
|
|
511
|
+
schema_location "gml.xsd"
|
|
512
|
+
end
|
|
513
|
+
measure = Class.new(Lutaml::Model::Type::String) do
|
|
514
|
+
xml { namespace gml_ns }
|
|
515
|
+
xsd_type "gml:MeasureType"
|
|
516
|
+
end
|
|
517
|
+
doc = Class.new(Lutaml::Model::Serializable) do
|
|
518
|
+
attribute :measure, measure
|
|
519
|
+
xml do
|
|
520
|
+
root "doc"
|
|
521
|
+
map_element "measure", to: :measure
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# skip_validation matches main's behavior for namespaced custom types;
|
|
526
|
+
# accepting them in validation is a deferred follow-up.
|
|
527
|
+
xsd = Lutaml::Model::Schema.to_xsd(doc, skip_validation: true)
|
|
528
|
+
|
|
529
|
+
expect(xsd).to include('xmlns:gml="http://www.opengis.net/gml"')
|
|
530
|
+
expect(xsd).to include('<xs:import namespace="http://www.opengis.net/gml" schemaLocation="gml.xsd"/>')
|
|
531
|
+
expect(xsd).to include('type="gml:MeasureType"')
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
it "synthesizes a non-colliding prefix when a foreign namespace claims tns" do
|
|
535
|
+
no_pfx_target = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
536
|
+
uri "http://example.com/np" # no prefix_default -> synthesized
|
|
537
|
+
end
|
|
538
|
+
tns_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
539
|
+
uri "http://t.example"
|
|
540
|
+
prefix_default "tns"
|
|
541
|
+
schema_location "t.xsd"
|
|
542
|
+
end
|
|
543
|
+
child = Class.new(Lutaml::Model::Serializable) do
|
|
544
|
+
attribute :v, :string
|
|
545
|
+
xml do
|
|
546
|
+
root "child"
|
|
547
|
+
namespace tns_ns
|
|
548
|
+
type_name "TType"
|
|
549
|
+
map_element "v", to: :v
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
doc = Class.new(Lutaml::Model::Serializable) do
|
|
553
|
+
attribute :child, child
|
|
554
|
+
attribute :id, :string
|
|
555
|
+
xml do
|
|
556
|
+
root "doc"
|
|
557
|
+
namespace no_pfx_target
|
|
558
|
+
type_name "DocType"
|
|
559
|
+
map_element "child", to: :child
|
|
560
|
+
map_element "id", to: :id
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
xsd = Lutaml::Model::Schema.to_xsd(doc)
|
|
565
|
+
|
|
566
|
+
expect(xsd).to include('xmlns:tns="http://t.example"')
|
|
567
|
+
expect(xsd).to include('xmlns:tns1="http://example.com/np"')
|
|
568
|
+
expect(xsd).to include('type="tns1:DocType"')
|
|
569
|
+
expect(xsd).to include('type="tns:TType"')
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# A foreign-namespaced nested model with NO type_name is inlined into this
|
|
573
|
+
# document (like a same-namespace model), not imported — so its namespace
|
|
574
|
+
# must not trigger the foreign-import schema_location requirement. The
|
|
575
|
+
# namespace walk and the element walk must agree that it is not a boundary.
|
|
576
|
+
it "does not require schema_location for an inlined (type_name-less) foreign model" do
|
|
577
|
+
inner_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
578
|
+
uri "http://inner.example/in"
|
|
579
|
+
prefix_default "in" # no schema_location, no type_name below
|
|
580
|
+
end
|
|
581
|
+
outer_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
582
|
+
uri "http://outer.example/out"
|
|
583
|
+
prefix_default "out"
|
|
584
|
+
end
|
|
585
|
+
inner = Class.new(Lutaml::Model::Serializable) do
|
|
586
|
+
attribute :label, :string
|
|
587
|
+
xml do
|
|
588
|
+
root "inner"
|
|
589
|
+
namespace inner_ns
|
|
590
|
+
map_element "label", to: :label
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
outer = Class.new(Lutaml::Model::Serializable) do
|
|
594
|
+
attribute :inner, inner
|
|
595
|
+
xml do
|
|
596
|
+
root "outer"
|
|
597
|
+
namespace outer_ns
|
|
598
|
+
map_element "inner", to: :inner
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
xsd = Lutaml::Model::Schema.to_xsd(outer)
|
|
603
|
+
|
|
604
|
+
# Inlined, not imported: its content appears here, no <xs:import> for it.
|
|
605
|
+
expect(xsd).to include('name="label"')
|
|
606
|
+
expect(xsd).not_to include("http://inner.example/in")
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
it "does not emit a spurious import for an inlined foreign model with a schema_location" do
|
|
610
|
+
inner_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
611
|
+
uri "http://inner2.example/in"
|
|
612
|
+
prefix_default "in"
|
|
613
|
+
schema_location "inner.xsd" # present, but model has no type_name
|
|
614
|
+
end
|
|
615
|
+
outer_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
616
|
+
uri "http://outer2.example/out"
|
|
617
|
+
prefix_default "out"
|
|
618
|
+
end
|
|
619
|
+
inner = Class.new(Lutaml::Model::Serializable) do
|
|
620
|
+
attribute :label, :string
|
|
621
|
+
xml do
|
|
622
|
+
root "inner"
|
|
623
|
+
namespace inner_ns
|
|
624
|
+
map_element "label", to: :label
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
outer = Class.new(Lutaml::Model::Serializable) do
|
|
628
|
+
attribute :inner, inner
|
|
629
|
+
xml do
|
|
630
|
+
root "outer"
|
|
631
|
+
namespace outer_ns
|
|
632
|
+
map_element "inner", to: :inner
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
xsd = Lutaml::Model::Schema.to_xsd(outer)
|
|
637
|
+
|
|
638
|
+
expect(xsd).to include('name="label"')
|
|
639
|
+
expect(xsd).not_to include("<xs:import")
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
# A prefix bound to two different namespaces — a foreign model vs a
|
|
643
|
+
# Type::Value type sharing a prefix_default — is unresolvable and must
|
|
644
|
+
# raise, not silently keep the first binding.
|
|
645
|
+
it "raises when a foreign model and a Type::Value share a prefix for different namespaces" do
|
|
646
|
+
model_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
647
|
+
uri "http://model.example/gml"
|
|
648
|
+
prefix_default "gml"
|
|
649
|
+
schema_location "model.xsd"
|
|
650
|
+
end
|
|
651
|
+
type_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
652
|
+
uri "http://type.example/gml"
|
|
653
|
+
prefix_default "gml"
|
|
654
|
+
schema_location "type.xsd"
|
|
655
|
+
end
|
|
656
|
+
measure = Class.new(Lutaml::Model::Type::String) do
|
|
657
|
+
xml { namespace type_ns }
|
|
658
|
+
xsd_type "gml:MeasureType"
|
|
659
|
+
end
|
|
660
|
+
widget = Class.new(Lutaml::Model::Serializable) do
|
|
661
|
+
attribute :label, :string
|
|
662
|
+
xml do
|
|
663
|
+
root "widget"
|
|
664
|
+
namespace model_ns
|
|
665
|
+
type_name "WidgetType"
|
|
666
|
+
map_element "label", to: :label
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
target_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
670
|
+
uri "http://doc.example/doc"
|
|
671
|
+
prefix_default "doc"
|
|
672
|
+
end
|
|
673
|
+
doc = Class.new(Lutaml::Model::Serializable) do
|
|
674
|
+
attribute :size, measure
|
|
675
|
+
attribute :widget, widget
|
|
676
|
+
xml do
|
|
677
|
+
root "doc"
|
|
678
|
+
namespace target_ns
|
|
679
|
+
map_element "size", to: :size
|
|
680
|
+
map_element "widget", to: :widget
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
# Raises even under skip_validation — a wrong-namespace QName is never
|
|
685
|
+
# recoverable, matching the foreign prefix-collision behavior.
|
|
686
|
+
expect do
|
|
687
|
+
Lutaml::Model::Schema.to_xsd(doc, skip_validation: true)
|
|
688
|
+
end.to raise_error(Lutaml::Model::Error, /two different namespaces/)
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# A collection of a type_name-less model renders as a placeholder `item`
|
|
692
|
+
# element, so nothing inside it is emitted — the namespace walk must not
|
|
693
|
+
# descend into it and import a foreign type the schema never references.
|
|
694
|
+
it "does not import namespaces reachable only through a placeholder collection" do
|
|
695
|
+
child_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
696
|
+
uri "http://child.example/ch"
|
|
697
|
+
prefix_default "ch" # foreign + type_name + NO schema_location
|
|
698
|
+
end
|
|
699
|
+
inner_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
700
|
+
uri "http://binner.example/b"
|
|
701
|
+
prefix_default "bin"
|
|
702
|
+
end
|
|
703
|
+
child = Class.new(Lutaml::Model::Serializable) do
|
|
704
|
+
attribute :v, :string
|
|
705
|
+
xml do
|
|
706
|
+
root "child"
|
|
707
|
+
namespace child_ns
|
|
708
|
+
type_name "ChildType"
|
|
709
|
+
map_element "v", to: :v
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
inner = Class.new(Lutaml::Model::Serializable) do
|
|
713
|
+
attribute :child, child
|
|
714
|
+
xml do
|
|
715
|
+
root "inner"
|
|
716
|
+
namespace inner_ns
|
|
717
|
+
map_element "child", to: :child
|
|
718
|
+
end
|
|
719
|
+
end
|
|
720
|
+
root_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
721
|
+
uri "http://root.example/r"
|
|
722
|
+
prefix_default "r"
|
|
723
|
+
end
|
|
724
|
+
root = Class.new(Lutaml::Model::Serializable) do
|
|
725
|
+
attribute :inners, inner, collection: true
|
|
726
|
+
xml do
|
|
727
|
+
root "root"
|
|
728
|
+
namespace root_ns
|
|
729
|
+
map_element "inner", to: :inners
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
xsd = Lutaml::Model::Schema.to_xsd(root)
|
|
734
|
+
|
|
735
|
+
# No spurious import/raise for the foreign child reachable only through
|
|
736
|
+
# the placeholder collection.
|
|
737
|
+
expect(xsd).not_to include("http://child.example/ch")
|
|
738
|
+
expect(xsd).not_to include("http://binner.example/b")
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
# Locks the deliberate skip-before-cycle-guard ordering: a model referenced
|
|
742
|
+
# BOTH as a placeholder collection and as a single inlined attribute must
|
|
743
|
+
# still be inlined via the single path — the placeholder reference must not
|
|
744
|
+
# consume the model's cycle-guard slot and mask the emitted one. Regardless
|
|
745
|
+
# of attribute order, the inlined model's nested named type stays defined.
|
|
746
|
+
%i[collection_first single_first].each do |order|
|
|
747
|
+
it "defines a nested type of a model also referenced as a placeholder collection (#{order})" do
|
|
748
|
+
child = Class.new(Lutaml::Model::Serializable) do
|
|
749
|
+
attribute :v, :string
|
|
750
|
+
xml do
|
|
751
|
+
root "child"
|
|
752
|
+
type_name "CType"
|
|
753
|
+
map_element "v", to: :v
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
mid = Class.new(Lutaml::Model::Serializable) do
|
|
757
|
+
attribute :child, child
|
|
758
|
+
xml do
|
|
759
|
+
root "mid"
|
|
760
|
+
map_element "child", to: :child
|
|
761
|
+
end
|
|
762
|
+
end
|
|
763
|
+
root = Class.new(Lutaml::Model::Serializable) do
|
|
764
|
+
if order == :collection_first
|
|
765
|
+
attribute :many, mid, collection: true
|
|
766
|
+
attribute :one, mid
|
|
767
|
+
else
|
|
768
|
+
attribute :one, mid
|
|
769
|
+
attribute :many, mid, collection: true
|
|
770
|
+
end
|
|
771
|
+
xml do
|
|
772
|
+
root "root"
|
|
773
|
+
map_element "many", to: :many
|
|
774
|
+
map_element "one", to: :one
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
xsd = Lutaml::Model::Schema.to_xsd(root)
|
|
779
|
+
|
|
780
|
+
# The single-inline path references CType, so CType must be defined —
|
|
781
|
+
# even when the placeholder collection references `mid` first.
|
|
782
|
+
expect(xsd).to include('<xs:complexType name="CType">')
|
|
783
|
+
expect { Nokogiri::XML::Schema(xsd) }.not_to raise_error
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
# When the root schema has no targetNamespace, the foreign-prefix error must
|
|
788
|
+
# not interpolate a nil target into a confusing "'' schema" string.
|
|
789
|
+
it "reports the no-namespace schema clearly in the foreign-prefix error" do
|
|
790
|
+
foreign_ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
791
|
+
uri "http://foreign.example/f"
|
|
792
|
+
prefix_default "xs" # reserved -> unusable prefix, triggers the error
|
|
793
|
+
end
|
|
794
|
+
child = Class.new(Lutaml::Model::Serializable) do
|
|
795
|
+
attribute :v, :string
|
|
796
|
+
xml do
|
|
797
|
+
root "child"
|
|
798
|
+
namespace foreign_ns
|
|
799
|
+
type_name "ChildType"
|
|
800
|
+
map_element "v", to: :v
|
|
801
|
+
end
|
|
802
|
+
end
|
|
803
|
+
root = Class.new(Lutaml::Model::Serializable) do
|
|
804
|
+
attribute :child, child
|
|
805
|
+
xml do
|
|
806
|
+
root "root" # no namespace -> target_uri is nil
|
|
807
|
+
map_element "child", to: :child
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
expect { Lutaml::Model::Schema.to_xsd(root) }
|
|
812
|
+
.to raise_error(Lutaml::Model::Error, /this no-namespace schema/)
|
|
813
|
+
end
|
|
814
|
+
end
|
|
55
815
|
end
|