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
@@ -12,6 +12,79 @@ module Lutaml
12
12
  include Lutaml::Model::Schema::SharedMethods
13
13
  extend Lutaml::Model::Schema::SharedMethods
14
14
 
15
+ # The XSD vocabulary namespace, routed through the project's namespace
16
+ # object so the declared prefix and every emitted prefix stay in sync.
17
+ # W3c::XsNamespace binds "xs" (matching the built-in xs:-prefixed type
18
+ # references) and opts out of the W3C-reserved-prefix warning.
19
+ def self.xsd_ns
20
+ @xsd_ns ||= Lutaml::Xml::W3c::XsNamespace.new
21
+ end
22
+
23
+ # Qualify an XSD structure element name with the bound prefix, so the
24
+ # prefix used on emitted elements always matches the declared xmlns.
25
+ def self.qn(local)
26
+ "#{xsd_ns.prefix}:#{local}"
27
+ end
28
+
29
+ # Emit an XSD structure element (<xs:local ...>) through the builder.
30
+ def self.xs(xml, local, attrs = nil, &block)
31
+ if attrs
32
+ xml.public_send(qn(local), attrs, &block)
33
+ else
34
+ xml.public_send(qn(local), &block)
35
+ end
36
+ end
37
+
38
+ # Whether a prefix can appear in emitted QNames: present and not the
39
+ # reserved XSD vocabulary prefix.
40
+ def self.usable_prefix?(prefix)
41
+ !prefix.nil? && !prefix.empty? && prefix != xsd_ns.prefix
42
+ end
43
+
44
+ # Per-generation namespace context threaded through the generate_*
45
+ # methods: the schema's target namespace URI and the prefix that
46
+ # qualifies named-type references into it.
47
+ Context = Struct.new(:target_prefix, :target_uri)
48
+
49
+ # The prefix of the schema's target namespace, or nil when the schema
50
+ # has no target namespace (named types then live in no-namespace and
51
+ # their references stay unprefixed). Never returns the reserved "xs".
52
+ def self.target_ns_prefix(xml_mapping)
53
+ prefix =
54
+ if xml_mapping&.namespace_class
55
+ xml_mapping.namespace_prefix ||
56
+ xml_mapping.namespace_class.prefix_default
57
+ elsif xml_mapping&.namespace_uri
58
+ xml_mapping.namespace_prefix
59
+ end
60
+
61
+ prefix if usable_prefix?(prefix)
62
+ end
63
+
64
+ # Qualify a named-type reference with the given prefix so it resolves
65
+ # to the namespace the type is defined in. Built-in refs (which already
66
+ # carry a prefix, e.g. "xs:string") and no-namespace schemas (prefix
67
+ # nil) are returned unchanged.
68
+ def self.qualify_type_ref(type_name, prefix)
69
+ return type_name if prefix.nil? || type_name.nil?
70
+ return type_name if type_name.include?(":")
71
+
72
+ "#{prefix}:#{type_name}"
73
+ end
74
+
75
+ # The prefix a named-type reference should carry. A foreign model's
76
+ # type lives in its own namespace, so the ref uses that namespace's
77
+ # prefix; a model in the target namespace (same URI or none) is defined
78
+ # in this schema, so the ref uses the target prefix.
79
+ def self.nested_ref_prefix(nested_mapping, ctx)
80
+ ns_class = nested_mapping&.namespace_class
81
+ return ctx.target_prefix unless ns_class
82
+ return ctx.target_prefix if ns_class.uri == ctx.target_uri
83
+
84
+ prefix = ns_class.prefix_default
85
+ usable_prefix?(prefix) ? prefix : ctx.target_prefix
86
+ end
87
+
15
88
  def self.generate(klass, options = {})
16
89
  register = extract_register_from(klass)
17
90
  xml_mapping = klass.mappings_for(:xml)
@@ -75,7 +148,11 @@ module Lutaml
75
148
  # @param klass [Class] The model class to validate
76
149
  # @param register [Register] The register for type resolution
77
150
  # @raise [UnresolvableTypeError] if any types cannot be resolved
78
- def self.validate_xsd_types!(klass, register)
151
+ def self.validate_xsd_types!(klass, register, seen = Set.new)
152
+ # Cycle guard: recursive models (A -> B -> A) would otherwise recurse
153
+ # forever. A class already being validated needs no re-validation.
154
+ return unless seen.add?(klass)
155
+
79
156
  errors = []
80
157
 
81
158
  klass.attributes.each do |name, attr|
@@ -95,7 +172,7 @@ module Lutaml
95
172
  # Recursively validate nested models
96
173
  if attr_type <= Lutaml::Model::Serialize
97
174
  begin
98
- validate_xsd_types!(attr_type, register)
175
+ validate_xsd_types!(attr_type, register, seen)
99
176
  rescue Lutaml::Model::UnresolvableTypeError => e
100
177
  errors << "In nested model #{attr_type.name}: #{e.message}"
101
178
  end
@@ -108,61 +185,47 @@ module Lutaml
108
185
  end
109
186
  end
110
187
 
111
- def self.generate_schema(xml, klass, xml_mapping, register, _options)
112
- schema_attrs = { xmlns: "http://www.w3.org/2001/XMLSchema" }
113
-
114
- # Add namespace metadata from XmlNamespace class if present
115
- if xml_mapping.namespace_class
116
- ns = xml_mapping.namespace_class
117
- schema_attrs[:targetNamespace] = ns.uri
118
- schema_attrs[:elementFormDefault] = ns.element_form_default.to_s
119
- schema_attrs[:attributeFormDefault] = ns.attribute_form_default.to_s
120
- schema_attrs[:version] = ns.version if ns.version
188
+ def self.generate_schema(xml, klass, xml_mapping, register, options)
189
+ skip_validation = options[:skip_validation]
190
+
191
+ # Bind the XSD vocabulary namespace to the "xs" prefix (via the
192
+ # namespace object, not a hardcoded string) so the xs:-prefixed type
193
+ # references and the prefixed structure elements are both declared.
194
+ schema_attrs = { xsd_ns.attr_name.to_sym => xsd_ns.uri }
195
+ schema_attrs.merge!(target_namespace_attrs(xml_mapping))
196
+ target_uri = schema_attrs[:targetNamespace]
197
+
198
+ referenced = referenced_namespaces(klass, register, target_uri)
199
+
200
+ # Named-type references into the target namespace need a usable
201
+ # prefix; synthesise a non-colliding one when the namespace class
202
+ # declares none.
203
+ target_prefix = target_ns_prefix(xml_mapping)
204
+ if target_uri && target_prefix.nil?
205
+ target_prefix = synthesize_target_prefix(referenced)
206
+ schema_attrs[:"xmlns:#{target_prefix}"] = target_uri
207
+ end
121
208
 
122
- # Add xmlns declarations for the target namespace
123
- prefix = xml_mapping.namespace_prefix || ns.prefix_default
124
- if prefix && !prefix.empty?
125
- schema_attrs[:"xmlns:#{prefix}"] = ns.uri
126
- end
127
- elsif xml_mapping.namespace_uri
128
- # Legacy: namespace URI without XmlNamespace class
129
- schema_attrs[:targetNamespace] = xml_mapping.namespace_uri
130
- schema_attrs[:elementFormDefault] = "unqualified"
131
- schema_attrs[:attributeFormDefault] = "unqualified"
209
+ declare_referenced_namespaces!(schema_attrs, referenced, target_uri,
210
+ skip_validation)
132
211
 
133
- if xml_mapping.namespace_prefix
134
- schema_attrs[:"xmlns:#{xml_mapping.namespace_prefix}"] =
135
- xml_mapping.namespace_uri
136
- end
137
- end
212
+ ctx = Context.new(target_prefix, target_uri)
138
213
 
139
- xml.schema(schema_attrs) do
140
- # Generate imports from XmlNamespace
214
+ xs(xml, "schema", schema_attrs) do
215
+ # Explicit imports declared on the root's XmlNamespace class.
216
+ imported_uris = Set.new
141
217
  if xml_mapping.namespace_class
142
- generate_imports(xml, xml_mapping.namespace_class)
218
+ imported_uris = generate_imports(xml, xml_mapping.namespace_class)
143
219
  generate_includes(xml, xml_mapping.namespace_class)
144
220
  end
145
221
 
146
- # Generate imports for Type namespaces
147
- type_namespaces = collect_type_namespaces(klass, register)
148
- type_namespaces.each do |ns_class|
149
- # Only import if different from target namespace
150
- next if ns_class.uri == schema_attrs[:targetNamespace]
151
-
152
- import_attrs = { namespace: ns_class.uri }
153
- if ns_class.schema_location
154
- import_attrs[:schemaLocation] =
155
- ns_class.schema_location
156
- end
157
- xml.import(import_attrs)
158
- end
222
+ generate_referenced_imports(xml, referenced, target_uri,
223
+ imported_uris)
159
224
 
160
- # Generate annotation if present
161
225
  if xml_mapping.documentation_text || xml_mapping.namespace_class&.documentation
162
226
  generate_annotation(xml, xml_mapping)
163
227
  end
164
228
 
165
- # Determine element name and type name for XSD pattern selection
166
229
  element_name = if has_explicit_xml_mapping?(klass, xml_mapping)
167
230
  xml_mapping.element_name || xml_mapping.root_element
168
231
  end
@@ -175,82 +238,264 @@ module Lutaml
175
238
  # Pattern 3: both element and type_name -> element + named complexType
176
239
 
177
240
  if element_name && type_name
178
- # Pattern 3: Both element and named type
179
- xml.element(name: element_name, type: type_name)
241
+ xs(xml, "element",
242
+ { name: element_name,
243
+ type: qualify_type_ref(type_name, target_prefix) })
180
244
  generate_complex_type(xml, klass, type_name, register,
181
- xml_mapping)
245
+ xml_mapping, ctx: ctx)
182
246
  elsif type_name && !element_name
183
- # Pattern 2: Type-only (no element)
184
247
  generate_complex_type(xml, klass, type_name, register,
185
- xml_mapping)
248
+ xml_mapping, ctx: ctx)
186
249
  else
187
- # Pattern 1: Anonymous inline (element with no type_name)
188
250
  # Use class name as fallback element name if not specified
189
251
  elem_name = element_name || klass.name
190
- xml.element(name: elem_name) do
191
- generate_complex_type_content(xml, klass, register, xml_mapping)
252
+ xs(xml, "element", { name: elem_name }) do
253
+ generate_complex_type_content(xml, klass, register, xml_mapping,
254
+ ctx: ctx)
192
255
  end
193
256
  end
194
257
 
195
- # Generate type definitions for nested models with type_name
196
- generate_nested_type_definitions(xml, klass, register)
258
+ generate_nested_type_definitions(xml, klass, register, ctx: ctx)
259
+ end
260
+ end
261
+
262
+ # targetNamespace, form defaults, and the target xmlns declaration,
263
+ # derived from the root mapping's namespace configuration.
264
+ def self.target_namespace_attrs(xml_mapping)
265
+ attrs = {}
266
+
267
+ if xml_mapping.namespace_class
268
+ ns = xml_mapping.namespace_class
269
+ attrs[:targetNamespace] = ns.uri
270
+ attrs[:elementFormDefault] = ns.element_form_default.to_s
271
+ attrs[:attributeFormDefault] = ns.attribute_form_default.to_s
272
+ attrs[:version] = ns.version if ns.version
273
+
274
+ prefix = xml_mapping.namespace_prefix || ns.prefix_default
275
+ attrs[:"xmlns:#{prefix}"] = ns.uri if usable_prefix?(prefix)
276
+ elsif xml_mapping.namespace_uri
277
+ # Legacy: namespace URI without XmlNamespace class
278
+ attrs[:targetNamespace] = xml_mapping.namespace_uri
279
+ attrs[:elementFormDefault] = "unqualified"
280
+ attrs[:attributeFormDefault] = "unqualified"
281
+
282
+ if usable_prefix?(xml_mapping.namespace_prefix)
283
+ attrs[:"xmlns:#{xml_mapping.namespace_prefix}"] =
284
+ xml_mapping.namespace_uri
285
+ end
197
286
  end
287
+
288
+ attrs
289
+ end
290
+
291
+ # A synthetic prefix for a target namespace that declares none,
292
+ # avoiding every prefix claimed by a referenced namespace.
293
+ def self.synthesize_target_prefix(referenced)
294
+ taken = (referenced[:foreign_models] + referenced[:type_values])
295
+ .filter_map(&:prefix_default)
296
+ candidate = "tns"
297
+ suffix = 0
298
+ candidate = "tns#{suffix += 1}" while taken.include?(candidate)
299
+ candidate
198
300
  end
199
301
 
302
+ # Declare xmlns:<prefix> for every namespace this schema references.
303
+ # Foreign model namespaces are strict: an unusable prefix or a prefix
304
+ # collision always raises (the emitted QNames would resolve to the
305
+ # wrong namespace), while a missing schema_location raises unless
306
+ # skip_validation downgrades it to a warning (the output stays
307
+ # structurally correct, only the import is unresolvable).
308
+ # Type::Value namespaces are declared best-effort (their xsd_type
309
+ # references are emitted verbatim).
310
+ def self.declare_referenced_namespaces!(schema_attrs, referenced, target_uri, skip_validation)
311
+ referenced[:foreign_models].each do |ns_class|
312
+ # An unusable or colliding prefix makes emitted QNames resolve to
313
+ # the wrong namespace — never recoverable, even under
314
+ # skip_validation.
315
+ error = foreign_prefix_error(ns_class, schema_attrs, target_uri)
316
+ raise Lutaml::Model::Error, error if error
317
+
318
+ # A missing schema_location leaves the import unresolvable but the
319
+ # output structurally correct — recoverable under skip_validation.
320
+ unless ns_class.schema_location
321
+ error = missing_schema_location_error(ns_class)
322
+ raise Lutaml::Model::Error, error unless skip_validation
323
+
324
+ warn "[Lutaml::Model] WARN: #{error} " \
325
+ "(skip_validation: emitting best-effort output)"
326
+ end
327
+
328
+ schema_attrs[:"xmlns:#{ns_class.prefix_default}"] = ns_class.uri
329
+ end
330
+
331
+ referenced[:type_values].each do |ns_class|
332
+ prefix = ns_class.prefix_default
333
+ next unless usable_prefix?(prefix)
334
+
335
+ # A prefix bound to two different namespaces emits QNames that
336
+ # resolve to the wrong one — the same unrecoverable collision the
337
+ # foreign path raises on, so raise here too rather than silently
338
+ # keeping the first binding.
339
+ error = prefix_collision_error(prefix, ns_class.uri, schema_attrs)
340
+ raise Lutaml::Model::Error, error if error
341
+
342
+ schema_attrs[:"xmlns:#{prefix}"] = ns_class.uri
343
+ end
344
+ end
345
+
346
+ # Why a foreign namespace's prefix cannot be used in this schema's
347
+ # QNames, or nil when it can.
348
+ def self.foreign_prefix_error(ns_class, schema_attrs, target_uri)
349
+ prefix = ns_class.prefix_default
350
+ unless usable_prefix?(prefix)
351
+ schema_desc = target_uri ? "the '#{target_uri}'" : "this no-namespace"
352
+ return "XSD generation: foreign namespace '#{ns_class.uri}' " \
353
+ "needs a usable prefix_default (not nil/empty/'xs') to " \
354
+ "be referenced from #{schema_desc} schema."
355
+ end
356
+
357
+ prefix_collision_error(prefix, ns_class.uri, schema_attrs)
358
+ end
359
+
360
+ # The collision message when +prefix+ is already bound to a different
361
+ # namespace than +uri+ in +schema_attrs+, or nil when there is no
362
+ # conflict.
363
+ def self.prefix_collision_error(prefix, uri, schema_attrs)
364
+ existing = schema_attrs[:"xmlns:#{prefix}"]
365
+ return unless existing && existing != uri
366
+
367
+ "XSD generation: namespace prefix '#{prefix}' is bound " \
368
+ "to two different namespaces (#{existing} and #{uri}). " \
369
+ "Give them distinct prefixes."
370
+ end
371
+
372
+ def self.missing_schema_location_error(ns_class)
373
+ "XSD generation: foreign namespace '#{ns_class.uri}' needs a " \
374
+ "schema_location so its imported types can be resolved."
375
+ end
376
+
377
+ # <xs:import> for every referenced foreign namespace, deduped by URI
378
+ # against the explicit imports already emitted.
379
+ def self.generate_referenced_imports(xml, referenced, target_uri, imported_uris)
380
+ (referenced[:foreign_models] + referenced[:type_values]).each do |ns_class|
381
+ next if ns_class.uri == target_uri
382
+ next unless imported_uris.add?(ns_class.uri)
383
+
384
+ emit_import(xml, ns_class)
385
+ end
386
+ end
387
+
388
+ # Emit <xs:import> for each namespace the root's XmlNamespace class
389
+ # explicitly declares. Returns the Set of imported URIs so the tree
390
+ # import loop can dedupe against them.
200
391
  def self.generate_imports(xml, namespace_class)
201
- return unless namespace_class.imports&.any?
392
+ imported = Set.new
393
+ return imported unless namespace_class.imports&.any?
202
394
 
203
395
  namespace_class.imports.each do |imported_ns|
204
- import_attrs = { namespace: imported_ns.uri }
205
- if imported_ns.schema_location
206
- import_attrs[:schemaLocation] =
207
- imported_ns.schema_location
208
- end
209
- xml.import(import_attrs)
396
+ next unless imported.add?(imported_ns.uri)
397
+
398
+ emit_import(xml, imported_ns)
399
+ end
400
+
401
+ imported
402
+ end
403
+
404
+ # Emit a single <xs:import> for a namespace class, with schemaLocation
405
+ # when the class declares one.
406
+ def self.emit_import(xml, ns_class)
407
+ import_attrs = { namespace: ns_class.uri }
408
+ if ns_class.schema_location
409
+ import_attrs[:schemaLocation] = ns_class.schema_location
210
410
  end
411
+ xs(xml, "import", import_attrs)
211
412
  end
212
413
 
213
414
  def self.generate_includes(xml, namespace_class)
214
415
  return unless namespace_class.includes&.any?
215
416
 
216
417
  namespace_class.includes.each do |schema_location|
217
- xml.include(schemaLocation: schema_location)
418
+ xs(xml, "include", { schemaLocation: schema_location })
218
419
  end
219
420
  end
220
421
 
221
422
  def self.generate_annotation(xml, xml_mapping)
222
- xml.annotation do
423
+ xs(xml, "annotation") do
223
424
  doc_text = xml_mapping.documentation_text
224
425
  doc_text ||= xml_mapping.namespace_class&.documentation if xml_mapping.namespace_class
225
426
 
226
- xml.documentation(doc_text) if doc_text
427
+ xs(xml, "documentation", doc_text) if doc_text
227
428
  end
228
429
  end
229
430
 
230
- def self.generate_nested_type_definitions(xml, klass, register)
431
+ def self.generate_nested_type_definitions(xml, klass, register, ctx:, seen: nil)
432
+ # Cycle guard, seeded with the root class (already defined by
433
+ # generate_schema) so it is never redefined via a back-reference.
434
+ seen ||= Set[klass]
435
+
231
436
  klass.attributes.each_value do |attr|
232
437
  attr_type = attr.type(register)
233
438
  next unless attr_type <= Lutaml::Model::Serialize
234
439
 
235
440
  nested_mapping = attr_type.mappings_for(:xml)
236
- nested_type_name = nested_mapping&.type_name_value
237
441
 
238
- # Generate type definition if nested model has type_name
442
+ # Skip models this document does not emit — checked before the cycle
443
+ # guard so an unemitted reference never consumes the model's slot and
444
+ # masks an emitted reference elsewhere. A collection of a type_name-
445
+ # less model is a placeholder; an imported foreign type is defined in
446
+ # its own schema document. A foreign model without a type_name is
447
+ # inlined here, so we still descend to define the types it references.
448
+ next if collection_placeholder?(attr, nested_mapping)
449
+ next if imported_foreign_type?(nested_mapping, ctx.target_uri)
450
+ next unless seen.add?(attr_type)
451
+
452
+ nested_type_name = nested_mapping&.type_name_value
239
453
  if nested_type_name
240
454
  generate_complex_type(xml, attr_type, nested_type_name, register,
241
- nested_mapping)
242
- # Recursively generate nested types
243
- generate_nested_type_definitions(xml, attr_type, register)
455
+ nested_mapping, ctx: ctx)
244
456
  end
457
+
458
+ generate_nested_type_definitions(xml, attr_type, register,
459
+ ctx: ctx, seen: seen)
245
460
  end
246
461
  end
247
462
 
248
- def self.generate_complex_type_content(xml, klass, register,
249
- xml_mapping)
250
- xml.complexType do
463
+ # Whether a nested model's mapping belongs to a namespace other than the
464
+ # schema's target namespace. A model with no namespace belongs to the
465
+ # target schema (not foreign).
466
+ def self.foreign_namespace?(mapping, target_uri)
467
+ ns_class = mapping&.namespace_class
468
+ return false unless ns_class
469
+
470
+ ns_class.uri != target_uri
471
+ end
472
+
473
+ # Whether a nested model is referenced across a schema-document boundary:
474
+ # it lives in a foreign namespace AND exposes a named type, so it is
475
+ # referenced by a prefixed QName and resolved through an <xs:import>.
476
+ # A foreign model WITHOUT a type_name has no named type to reference, so
477
+ # generate_elements inlines it into this document — it is not a boundary,
478
+ # and both the namespace walk and the type-definition walk must descend
479
+ # into it rather than treat it as imported.
480
+ def self.imported_foreign_type?(mapping, target_uri)
481
+ foreign_namespace?(mapping, target_uri) &&
482
+ !mapping.type_name_value.nil?
483
+ end
484
+
485
+ # Whether a nested model is emitted only as a placeholder: a collection
486
+ # of a type_name-less model renders as `<element name="item"
487
+ # type="xs:string"/>`, so the model's own content is never inlined or
488
+ # referenced. This document therefore neither declares its namespaces
489
+ # nor defines its nested types — the walkers must not descend into it.
490
+ def self.collection_placeholder?(attr, mapping)
491
+ attr.collection? && mapping&.type_name_value.nil?
492
+ end
493
+
494
+ def self.generate_complex_type_content(xml, klass, register, xml_mapping, ctx:)
495
+ xs(xml, "complexType") do
251
496
  if klass.attributes.any?
252
- xml.sequence do
253
- generate_elements(xml, klass, register, xml_mapping)
497
+ xs(xml, "sequence") do
498
+ generate_elements(xml, klass, register, xml_mapping, ctx: ctx)
254
499
  end
255
500
  end
256
501
  if xml_mapping
@@ -260,19 +505,18 @@ xml_mapping)
260
505
  end
261
506
  end
262
507
 
263
- def self.generate_complex_type(xml, klass, type_name, register,
264
- xml_mapping = nil)
265
- xml.complexType(name: type_name) do
508
+ def self.generate_complex_type(xml, klass, type_name, register, xml_mapping = nil, ctx:)
509
+ xs(xml, "complexType", { name: type_name }) do
266
510
  if klass.attributes.any?
267
- xml.sequence do
268
- generate_elements(xml, klass, register, xml_mapping)
511
+ xs(xml, "sequence") do
512
+ generate_elements(xml, klass, register, xml_mapping, ctx: ctx)
269
513
  end
270
514
  end
271
515
  generate_attributes(xml, klass, register, xml_mapping)
272
516
  end
273
517
  end
274
518
 
275
- def self.generate_elements(xml, klass, register, xml_mapping)
519
+ def self.generate_elements(xml, klass, register, xml_mapping, ctx:)
276
520
  klass.attributes.each do |name, attr|
277
521
  next if xml_mapping && attr_is_xml_attribute?(xml_mapping, name)
278
522
 
@@ -293,26 +537,36 @@ xml_mapping = nil)
293
537
  element_attrs[:maxOccurs] = "unbounded"
294
538
 
295
539
  if nested_type_name
296
- # Reference named type
297
- element_attrs[:type] = nested_type_name
298
- xml.element(element_attrs)
540
+ # Reference named type by its owning namespace's prefix
541
+ element_attrs[:type] = qualify_type_ref(
542
+ nested_type_name,
543
+ nested_ref_prefix(nested_mapping, ctx),
544
+ )
545
+ xs(xml, "element", element_attrs)
299
546
  else
300
547
  # Inline anonymous complexType
301
- xml.element(element_attrs) do
302
- xml.complexType do
303
- xml.sequence do
304
- xml.element(name: "item", type: get_xsd_type(attr_type))
548
+ xs(xml, "element", element_attrs) do
549
+ xs(xml, "complexType") do
550
+ xs(xml, "sequence") do
551
+ xs(xml, "element",
552
+ { name: "item", type: get_xsd_type(attr_type) })
305
553
  end
306
554
  end
307
555
  end
308
556
  end
309
557
  elsif nested_type_name
310
- # Single nested model - Reference named type
311
- xml.element(name: name.to_s, type: nested_type_name)
558
+ # Single nested model - reference by its owning namespace prefix
559
+ xs(xml, "element",
560
+ { name: name.to_s,
561
+ type: qualify_type_ref(
562
+ nested_type_name,
563
+ nested_ref_prefix(nested_mapping, ctx),
564
+ ) })
312
565
  else
313
566
  # Inline anonymous complexType
314
- xml.element(name: name.to_s) do
315
- generate_complex_type_content(xml, attr_type, register, nil)
567
+ xs(xml, "element", { name: name.to_s }) do
568
+ generate_complex_type_content(xml, attr_type, register, nil,
569
+ ctx: ctx)
316
570
  end
317
571
  end
318
572
  else
@@ -326,10 +580,10 @@ xml_mapping = nil)
326
580
  element_attrs[:minOccurs] = "0"
327
581
  element_attrs[:maxOccurs] = "unbounded"
328
582
 
329
- xml.element(element_attrs) do
330
- xml.complexType do
331
- xml.sequence do
332
- xml.element(name: "item", type: xsd_type)
583
+ xs(xml, "element", element_attrs) do
584
+ xs(xml, "complexType") do
585
+ xs(xml, "sequence") do
586
+ xs(xml, "element", { name: "item", type: xsd_type })
333
587
  end
334
588
  end
335
589
  end
@@ -337,7 +591,7 @@ xml_mapping = nil)
337
591
  # Simple element
338
592
  element_attrs = build_element_attributes(name, xsd_type, attr,
339
593
  xml_mapping, name)
340
- xml.element(element_attrs)
594
+ xs(xml, "element", element_attrs)
341
595
  end
342
596
  end
343
597
  end
@@ -358,13 +612,13 @@ xml_mapping = nil)
358
612
  attr_attrs[:form] = rule.form.to_s if rule.form
359
613
 
360
614
  if rule.documentation
361
- xml.attribute(attr_attrs) do
362
- xml.annotation do
363
- xml.documentation(rule.documentation)
615
+ xs(xml, "attribute", attr_attrs) do
616
+ xs(xml, "annotation") do
617
+ xs(xml, "documentation", rule.documentation)
364
618
  end
365
619
  end
366
620
  else
367
- xml.attribute(attr_attrs)
621
+ xs(xml, "attribute", attr_attrs)
368
622
  end
369
623
  end
370
624
  end
@@ -432,32 +686,58 @@ _mapping_rule = nil)
432
686
  get_xsd_type(attr_type)
433
687
  end
434
688
 
435
- def self.collect_type_namespaces(klass, register)
436
- namespaces = Set.new
689
+ # Namespaces referenced by THIS schema document, collected by walking
690
+ # the model tree from the root class (structural, no instance needed).
691
+ # The walk stops at foreign-model boundaries: a foreign model's type is
692
+ # imported — defined in its own schema document — so namespaces used
693
+ # only inside it are that document's concern, not this one's.
694
+ #
695
+ # @return [Hash] :foreign_models — namespace classes of directly
696
+ # referenced foreign models (their type refs are prefixed QNames
697
+ # resolved through an import); :type_values — namespace classes
698
+ # declared by Type::Value attribute types (imported so their
699
+ # verbatim xsd_type references can resolve). Both deduped by class.
700
+ def self.referenced_namespaces(klass, register, target_uri, seen = Set.new)
701
+ result = { foreign_models: [], type_values: [] }
702
+ return result unless klass.is_a?(::Class) && seen.add?(klass)
437
703
 
438
704
  klass.attributes.each_value do |attr|
439
705
  type_class = attr.type(register)
440
706
  next unless type_class
441
707
 
442
- # Use unified get_namespace_info method
443
- ns_info = get_namespace_info(type_class)
444
- namespaces << ns_info[:class] if ns_info[:class]
708
+ if type_class <= Lutaml::Model::Serialize
709
+ mapping = type_class.mappings_for(:xml)
710
+ if imported_foreign_type?(mapping, target_uri)
711
+ result[:foreign_models] << mapping.namespace_class
712
+ elsif collection_placeholder?(attr, mapping)
713
+ # Placeholder collection: its content is never emitted, so it
714
+ # contributes no namespaces to this document.
715
+ next
716
+ else
717
+ nested = referenced_namespaces(type_class, register,
718
+ target_uri, seen)
719
+ result[:foreign_models].concat(nested[:foreign_models])
720
+ result[:type_values].concat(nested[:type_values])
721
+ end
722
+ else
723
+ ns = get_namespace_info(type_class)[:class]
724
+ if ns.is_a?(::Class) && ns < Lutaml::Xml::Namespace
725
+ result[:type_values] << ns
726
+ end
727
+ end
445
728
  end
446
729
 
447
- namespaces.to_a
730
+ result[:foreign_models].uniq!
731
+ result[:type_values].uniq!
732
+ result
448
733
  end
449
734
 
450
- # Get unified namespace information from Model or Type class
735
+ # Namespace information for a Type::Value class (the only kind the
736
+ # namespace walk queries — nested models are handled structurally by
737
+ # referenced_namespaces). Returns {} for anything else.
451
738
  def self.get_namespace_info(klass)
452
739
  return {} unless klass.is_a?(::Class)
453
740
 
454
- # Check for Model class (Serializable)
455
- if defined?(Lutaml::Model::Serialize) &&
456
- klass <= Lutaml::Model::Serialize
457
- return get_model_namespace_info(klass)
458
- end
459
-
460
- # Check for Type class (Type::Value)
461
741
  if defined?(Lutaml::Model::Type::Value) &&
462
742
  klass <= Lutaml::Model::Type::Value
463
743
  return get_type_namespace_info(klass)
@@ -469,18 +749,6 @@ _mapping_rule = nil)
469
749
  class << self
470
750
  private
471
751
 
472
- # Get namespace info from Model class (Serializable)
473
- def get_model_namespace_info(klass)
474
- mapping = klass.is_a?(Class) && klass.include?(Lutaml::Model::Serialize) ? klass.mappings_for(:xml) : nil
475
- return {} unless mapping
476
-
477
- {
478
- uri: mapping.namespace_uri,
479
- prefix: mapping.namespace_prefix,
480
- class: mapping.namespace_class,
481
- }
482
- end
483
-
484
752
  # Get namespace info from Type class (Type::Value)
485
753
  def get_type_namespace_info(klass)
486
754
  ns = klass.is_a?(Class) && klass <= Lutaml::Model::Type::Value ? klass.namespace_class : nil