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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +25 -23
  3. data/docs/_pages/breaking-changes.adoc +58 -0
  4. data/docs/_pages/collections.adoc +25 -24
  5. data/docs/_pages/validation.adoc +48 -1
  6. data/lib/compat/opal/lutaml_model_boot.rb +1 -0
  7. data/lib/lutaml/key_value/transform.rb +7 -1
  8. data/lib/lutaml/model/attribute.rb +67 -24
  9. data/lib/lutaml/model/collection.rb +5 -2
  10. data/lib/lutaml/model/collection_handler.rb +45 -2
  11. data/lib/lutaml/model/serialize/enum_handling.rb +5 -1
  12. data/lib/lutaml/model/validation.rb +49 -4
  13. data/lib/lutaml/model/version.rb +1 -1
  14. data/lib/lutaml/xml/format.rb +2 -0
  15. data/lib/lutaml/xml/model_transform.rb +33 -9
  16. data/lib/lutaml/xml/nested_collection_attribute.rb +95 -0
  17. data/lib/lutaml/xml/schema/xsd_schema.rb +396 -128
  18. data/lib/lutaml/xml/serialization/instance_methods.rb +2 -2
  19. data/lib/lutaml/xml/transformation/element_builder.rb +55 -8
  20. data/spec/lutaml/model/attribute_collection_spec.rb +156 -0
  21. data/spec/lutaml/model/attribute_spec.rb +1 -1
  22. data/spec/lutaml/model/cardinality_strict_spec.rb +614 -0
  23. data/spec/lutaml/model/cdata_spec.rb +1 -1
  24. data/spec/lutaml/model/collection_register_spec.rb +119 -0
  25. data/spec/lutaml/model/collection_single_value_coercion_spec.rb +222 -0
  26. data/spec/lutaml/model/root_mappings_spec.rb +10 -7
  27. data/spec/lutaml/model/schema/xsd_schema_spec.rb +785 -25
  28. data/spec/lutaml/model/serializable_validation_spec.rb +293 -0
  29. data/spec/lutaml/model/value_policy_spec.rb +6 -3
  30. data/spec/lutaml/model/xsd_form_default_patterns_spec.rb +12 -12
  31. data/spec/lutaml/model/xsd_patterns_spec.rb +29 -29
  32. data/spec/lutaml/model/xsd_type_validation_spec.rb +1 -1
  33. data/spec/lutaml/turtle/transform_spec.rb +2 -2
  34. 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