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
|
@@ -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,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
|
140
|
-
#
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
392
|
+
imported = Set.new
|
|
393
|
+
return imported unless namespace_class.imports&.any?
|
|
202
394
|
|
|
203
395
|
namespace_class.imports.each do |imported_ns|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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] =
|
|
298
|
-
|
|
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
|
|
302
|
-
xml
|
|
303
|
-
xml
|
|
304
|
-
xml
|
|
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 -
|
|
311
|
-
xml
|
|
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
|
|
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
|
|
330
|
-
xml
|
|
331
|
-
xml
|
|
332
|
-
xml
|
|
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
|
|
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
|
|
362
|
-
xml
|
|
363
|
-
xml
|
|
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
|
|
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
|
-
|
|
436
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
730
|
+
result[:foreign_models].uniq!
|
|
731
|
+
result[:type_values].uniq!
|
|
732
|
+
result
|
|
448
733
|
end
|
|
449
734
|
|
|
450
|
-
#
|
|
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
|