lutaml-xsd 1.1.13 → 1.2.0
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/.gitignore +0 -1
- data/CLAUDE.md +18 -1
- data/Gemfile +2 -1
- data/Gemfile.lock +327 -0
- data/Rakefile +78 -1
- data/TODO.VISION.md +123 -0
- data/TODO.cleanup.md +43 -0
- data/TODO.conformance.md +99 -0
- data/TODO.docs-spa.md +46 -0
- data/TODO.interop.md +52 -0
- data/TODO.performance.md +80 -0
- data/TODO.som.md +47 -0
- data/benchmark/engines.rb +77 -0
- data/config/validation.yml +4 -0
- data/docs/CONFORMANCE.adoc +43 -0
- data/docs/JSON_SCHEMA.adoc +42 -0
- data/docs/superpowers/plans/2026-10-20-moxml-leptris-migration.md +735 -0
- data/docs/superpowers/specs/2026-10-20-moxml-leptris-migration-design.md +169 -0
- data/docs/superpowers/specs/2026-10-21-rule-engine-wiring-design.md +84 -0
- data/lib/lutaml/xsd/conformance/runner.rb +169 -0
- data/lib/lutaml/xsd/json_schema_exporter.rb +240 -0
- data/lib/lutaml/xsd/package_builder.rb +2 -2
- data/lib/lutaml/xsd/spa/schema_serializer.rb +12 -7
- data/lib/lutaml/xsd/validation/engine_context.rb +190 -0
- data/lib/lutaml/xsd/validation/rule_engine.rb +36 -18
- data/lib/lutaml/xsd/validation/rule_registry.rb +34 -0
- data/lib/lutaml/xsd/validation/rules/attribute_validation_rule.rb +126 -17
- data/lib/lutaml/xsd/validation/rules/content_model_validation_rule.rb +304 -194
- data/lib/lutaml/xsd/validation/rules/element_structure_rule.rb +25 -15
- data/lib/lutaml/xsd/validation/rules/identity_constraints_rule.rb +134 -0
- data/lib/lutaml/xsd/validation/rules/occurrence_validation_rule.rb +76 -25
- data/lib/lutaml/xsd/validation/rules/type_validation_rule.rb +134 -17
- data/lib/lutaml/xsd/validation/schema_definition_resolver.rb +254 -0
- data/lib/lutaml/xsd/validation/validation_configuration.rb +1 -0
- data/lib/lutaml/xsd/validation/validation_job.rb +70 -3
- data/lib/lutaml/xsd/validation/validation_rule.rb +44 -5
- data/lib/lutaml/xsd/validation/validator.rb +2 -1
- data/lib/lutaml/xsd/validation/xml_element.rb +2 -2
- data/lib/lutaml/xsd/version.rb +1 -1
- data/lib/lutaml/xsd.rb +14 -0
- data/lutaml-xsd.gemspec +2 -2
- metadata +27 -8
|
@@ -47,95 +47,119 @@ module Lutaml
|
|
|
47
47
|
# The schema definition
|
|
48
48
|
# @param collector [ResultCollector] Collector for validation results
|
|
49
49
|
# @return [void]
|
|
50
|
-
def validate(xml_element, schema_def, collector)
|
|
50
|
+
def validate(xml_element, schema_def, collector, repository: nil)
|
|
51
51
|
return unless schema_def
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
validate_all(xml_element, content_model, collector)
|
|
65
|
-
end
|
|
53
|
+
return if engine_content_check(xml_element, collector) == :handled
|
|
54
|
+
|
|
55
|
+
particles = effective_particles(
|
|
56
|
+
xml_element, schema_def, repository
|
|
57
|
+
)
|
|
58
|
+
# nil: an xsi:type override that does not resolve — the type
|
|
59
|
+
# rule already reports it, so no content walk runs here.
|
|
60
|
+
return if particles.nil? || particles.empty?
|
|
61
|
+
|
|
62
|
+
validate_particle_sequence(xml_element, particles, collector,
|
|
63
|
+
repository)
|
|
66
64
|
end
|
|
67
65
|
|
|
68
66
|
private
|
|
69
67
|
|
|
70
|
-
#
|
|
68
|
+
# Particles of the effective type: an xsi:type override wins over
|
|
69
|
+
# the declared type; the walk then consumes the list in order.
|
|
71
70
|
#
|
|
72
|
-
# @
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return
|
|
71
|
+
# @return [Array, nil] nil when an xsi:type override does not
|
|
72
|
+
# resolve to a known type
|
|
73
|
+
def effective_particles(xml_element, schema_def, repository)
|
|
74
|
+
xsi = repository && xsi_attribute(xml_element, "type")
|
|
75
|
+
if xsi&.value.to_s.match?(/\S/)
|
|
76
|
+
local = xsi.value.to_s.split(":").last
|
|
77
|
+
named = repository.resolve_named_type(local)
|
|
78
|
+
return nil unless named
|
|
79
|
+
|
|
80
|
+
return type_particles(named, repository, {}.compare_by_identity)
|
|
80
81
|
end
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
case schema_def
|
|
84
|
+
when Lutaml::Xml::Schema::Xsd::Element
|
|
85
|
+
type_def = resolve_type(schema_def, repository)
|
|
86
|
+
return [] unless type_def
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
type_particles(type_def, repository, {}.compare_by_identity)
|
|
89
|
+
when Lutaml::Xsd::Sequence, Lutaml::Xsd::Choice, Lutaml::Xsd::All
|
|
90
|
+
# A rooted group walks as a single particle: the sequence
|
|
91
|
+
# walk's container handling then matches any leaf in any
|
|
92
|
+
# consecutive run, which is permissive-correct for choice
|
|
93
|
+
# and all without the old ambiguity false positive.
|
|
94
|
+
[schema_def]
|
|
95
|
+
else
|
|
96
|
+
type_particles(schema_def, repository, {}.compare_by_identity)
|
|
90
97
|
end
|
|
91
|
-
|
|
92
|
-
nil
|
|
93
98
|
end
|
|
94
99
|
|
|
95
|
-
#
|
|
100
|
+
# Ordered particles of a type: own sequence/choice/all/element/
|
|
101
|
+
# any/group particles plus, for extensions, the base type's
|
|
102
|
+
# particles first. Cycle-guarded. Particles are kept whole —
|
|
103
|
+
# nested containers are expanded by the walk's matching, which
|
|
104
|
+
# preserves their own occurrence constraints.
|
|
96
105
|
#
|
|
97
|
-
# @
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return element.complex_type if element.complex_type
|
|
106
|
+
# @return [Array]
|
|
107
|
+
def type_particles(type, repository, seen)
|
|
108
|
+
return [] if type.nil? || seen.key?(type)
|
|
101
109
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
end
|
|
110
|
+
seen[type] = true
|
|
111
|
+
particles = []
|
|
105
112
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
%i[sequence choice all element any group].each do |kind|
|
|
114
|
+
next unless type.respond_to?(kind)
|
|
115
|
+
|
|
116
|
+
Array(type.public_send(kind)).each do |particle|
|
|
117
|
+
next unless particle
|
|
118
|
+
|
|
119
|
+
# A sequence's direct particles join the walk in order;
|
|
120
|
+
# choice/all/any/element/group stay whole so their own
|
|
121
|
+
# occurrence constraints survive the walk.
|
|
122
|
+
if particle.is_a?(Lutaml::Xsd::Sequence)
|
|
123
|
+
particles.concat(collect_particles(particle))
|
|
124
|
+
else
|
|
125
|
+
particles << particle
|
|
126
|
+
end
|
|
127
|
+
end
|
|
117
128
|
end
|
|
118
129
|
|
|
119
|
-
if complex_content.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
if type.respond_to?(:complex_content) && type.complex_content
|
|
131
|
+
content = type.complex_content
|
|
132
|
+
extension = Array(content.extension).first
|
|
133
|
+
if extension
|
|
134
|
+
base_local = extension.base.to_s.split(":").last
|
|
135
|
+
base_type = base_local && repository&.resolve_named_type(base_local)
|
|
136
|
+
particles = type_particles(base_type, repository, seen) +
|
|
137
|
+
particles
|
|
138
|
+
end
|
|
139
|
+
%i[sequence choice all element any group].each do |kind|
|
|
140
|
+
next unless extension.respond_to?(kind)
|
|
141
|
+
|
|
142
|
+
Array(extension.public_send(kind)).each do |particle|
|
|
143
|
+
next unless particle
|
|
144
|
+
|
|
145
|
+
if particle.is_a?(Lutaml::Xsd::Sequence)
|
|
146
|
+
particles.concat(collect_particles(particle))
|
|
147
|
+
else
|
|
148
|
+
particles << particle
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
124
152
|
end
|
|
125
153
|
|
|
126
|
-
|
|
154
|
+
particles
|
|
127
155
|
end
|
|
128
156
|
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
# Elements must appear in the order defined by the sequence.
|
|
157
|
+
# Sequential walk over a particle list (min/max per particle,
|
|
158
|
+
# recursive matching, wildcard permission for unmatched tails).
|
|
132
159
|
#
|
|
133
|
-
# @param xml_element [XmlElement] The XML element
|
|
134
|
-
# @param sequence [Lutaml::Xsd::Sequence] The sequence definition
|
|
135
|
-
# @param collector [ResultCollector] Result collector
|
|
136
160
|
# @return [void]
|
|
137
|
-
def
|
|
138
|
-
|
|
161
|
+
def validate_particle_sequence(xml_element, particles, collector,
|
|
162
|
+
repository)
|
|
139
163
|
return if particles.empty?
|
|
140
164
|
|
|
141
165
|
children = xml_element.children
|
|
@@ -143,14 +167,30 @@ module Lutaml
|
|
|
143
167
|
|
|
144
168
|
particles.each do |particle|
|
|
145
169
|
min_occurs = parse_occurs(particle.min_occurs, 1)
|
|
146
|
-
|
|
170
|
+
# Container particles (nested sequence/choice/all) consume
|
|
171
|
+
# their whole consecutive run: the walk counts individual
|
|
172
|
+
# children, so a default max of 1 would truncate the group
|
|
173
|
+
# after its first child. Explicit caps on containers are
|
|
174
|
+
# enforced loosely for the same reason.
|
|
175
|
+
container = particle.is_a?(Lutaml::Xsd::Sequence) ||
|
|
176
|
+
particle.is_a?(Lutaml::Xsd::Choice) ||
|
|
177
|
+
particle.is_a?(Lutaml::Xsd::All)
|
|
178
|
+
max_occurs = parse_max_occurs(
|
|
179
|
+
particle.max_occurs, container ? :unbounded : 1
|
|
180
|
+
)
|
|
147
181
|
matched_count = 0
|
|
148
182
|
|
|
149
183
|
# Try to match children against this particle
|
|
150
184
|
while child_index < children.size
|
|
151
185
|
child = children[child_index]
|
|
152
186
|
|
|
153
|
-
break unless particle_matches?(child, particle
|
|
187
|
+
break unless particle_matches?(child, particle, repository,
|
|
188
|
+
parent: xml_element)
|
|
189
|
+
|
|
190
|
+
if particle.is_a?(Lutaml::Xsd::Any)
|
|
191
|
+
check_strict_wildcard_child(child, particle, collector,
|
|
192
|
+
repository)
|
|
193
|
+
end
|
|
154
194
|
|
|
155
195
|
matched_count += 1
|
|
156
196
|
child_index += 1
|
|
@@ -160,32 +200,29 @@ module Lutaml
|
|
|
160
200
|
matched_count >= max_occurs
|
|
161
201
|
|
|
162
202
|
# Child doesn't match, move to next particle
|
|
163
|
-
|
|
164
203
|
end
|
|
165
204
|
|
|
166
|
-
# Check minimum
|
|
205
|
+
# Check minimum occurrence
|
|
167
206
|
next unless matched_count < min_occurs
|
|
207
|
+
next if particle.is_a?(Lutaml::Xsd::Any) && min_occurs.zero?
|
|
168
208
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
code: "sequence_min_occurs_violation",
|
|
172
|
-
message: "Element '#{particle_name(particle)}' must occur " \
|
|
173
|
-
"at least #{min_occurs} time(s) in sequence, " \
|
|
174
|
-
"found #{matched_count}",
|
|
175
|
-
location: xml_element.xpath,
|
|
176
|
-
context: {
|
|
177
|
-
particle: particle_name(particle),
|
|
178
|
-
min_occurs: min_occurs,
|
|
179
|
-
actual: matched_count,
|
|
180
|
-
},
|
|
181
|
-
)
|
|
209
|
+
report_min_occurs_violation(particle, xml_element, collector,
|
|
210
|
+
min_occurs, matched_count)
|
|
182
211
|
end
|
|
183
212
|
|
|
184
213
|
# Check for unexpected children after sequence
|
|
185
214
|
return unless child_index < children.size
|
|
186
215
|
|
|
216
|
+
wildcard = particles.find { |p| p.is_a?(Lutaml::Xsd::Any) }
|
|
187
217
|
unexpected = children[child_index..]
|
|
188
218
|
unexpected.each do |child|
|
|
219
|
+
if wildcard && wildcard_permits?(wildcard, xml_element, child,
|
|
220
|
+
repository)
|
|
221
|
+
check_strict_wildcard_child(child, wildcard, collector,
|
|
222
|
+
repository)
|
|
223
|
+
next
|
|
224
|
+
end
|
|
225
|
+
|
|
189
226
|
report_error(
|
|
190
227
|
collector,
|
|
191
228
|
code: "unexpected_element_in_sequence",
|
|
@@ -200,118 +237,149 @@ module Lutaml
|
|
|
200
237
|
end
|
|
201
238
|
end
|
|
202
239
|
|
|
203
|
-
#
|
|
240
|
+
# Report an unsatisfied minimum occurrence: a choice container
|
|
241
|
+
# names its alternatives, anything else reports generically.
|
|
204
242
|
#
|
|
205
|
-
# One (and only one) of the alternatives must be present.
|
|
206
|
-
#
|
|
207
|
-
# @param xml_element [XmlElement] The XML element
|
|
208
|
-
# @param choice [Lutaml::Xsd::Choice] The choice definition
|
|
209
|
-
# @param collector [ResultCollector] Result collector
|
|
210
243
|
# @return [void]
|
|
211
|
-
def
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
matched_particles = []
|
|
217
|
-
|
|
218
|
-
# Find which particles match
|
|
219
|
-
particles.each do |particle|
|
|
220
|
-
matches = children.select do |child|
|
|
221
|
-
particle_matches?(child, particle)
|
|
244
|
+
def report_min_occurs_violation(particle, xml_element, collector,
|
|
245
|
+
min_occurs, matched_count)
|
|
246
|
+
if particle.is_a?(Lutaml::Xsd::Choice)
|
|
247
|
+
alternatives = collect_particles(particle).map do |p|
|
|
248
|
+
particle_name(p)
|
|
222
249
|
end
|
|
223
|
-
matched_particles << particle if matches.any?
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
min_occurs = parse_occurs(choice.min_occurs, 1)
|
|
227
|
-
|
|
228
|
-
# Check if at least one alternative is present
|
|
229
|
-
if matched_particles.empty? && min_occurs.positive?
|
|
230
|
-
particle_names = particles.map { |p| particle_name(p) }
|
|
231
250
|
report_error(
|
|
232
251
|
collector,
|
|
233
252
|
code: "choice_not_satisfied",
|
|
234
253
|
message: "One of the following elements must be present: " \
|
|
235
|
-
"#{
|
|
254
|
+
"#{alternatives.join(', ')}",
|
|
236
255
|
location: xml_element.xpath,
|
|
237
|
-
context: {
|
|
238
|
-
|
|
239
|
-
},
|
|
240
|
-
suggestion: "Add one of: #{particle_names.join(', ')}",
|
|
256
|
+
context: { choices: alternatives },
|
|
257
|
+
suggestion: "Add one of: #{alternatives.join(', ')}",
|
|
241
258
|
)
|
|
242
|
-
|
|
243
|
-
# Multiple alternatives present (ambiguous choice)
|
|
259
|
+
else
|
|
244
260
|
report_error(
|
|
245
261
|
collector,
|
|
246
|
-
code: "
|
|
247
|
-
message: "
|
|
248
|
-
"
|
|
262
|
+
code: "sequence_min_occurs_violation",
|
|
263
|
+
message: "Element '#{particle_name(particle)}' must occur " \
|
|
264
|
+
"at least #{min_occurs} time(s)",
|
|
249
265
|
location: xml_element.xpath,
|
|
250
266
|
context: {
|
|
251
|
-
|
|
267
|
+
particle: particle_name(particle),
|
|
268
|
+
min_occurs: min_occurs,
|
|
269
|
+
actual: matched_count,
|
|
252
270
|
},
|
|
253
|
-
suggestion: "Use only one of the choice alternatives",
|
|
254
271
|
)
|
|
255
272
|
end
|
|
256
273
|
end
|
|
257
274
|
|
|
258
|
-
#
|
|
259
|
-
#
|
|
260
|
-
# All elements must appear, but order doesn't matter.
|
|
275
|
+
# Engine-first content-model check for this element.
|
|
261
276
|
#
|
|
262
277
|
# @param xml_element [XmlElement] The XML element
|
|
263
|
-
# @param
|
|
264
|
-
# @
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
278
|
+
# @param collector [ResultCollector] Collector for results
|
|
279
|
+
# @return [:handled, :skipped] :skipped lets the Ruby walk run
|
|
280
|
+
def engine_content_check(xml_element, collector)
|
|
281
|
+
engine = @options[:engine_context]
|
|
282
|
+
return :skipped unless engine&.supported?
|
|
283
|
+
|
|
284
|
+
answer = engine.content_valid(
|
|
285
|
+
xml_element.name, xml_element.moxml_element.children.to_a
|
|
286
|
+
)
|
|
287
|
+
return :skipped if answer.nil?
|
|
288
|
+
|
|
289
|
+
unless answer
|
|
290
|
+
report_error(
|
|
291
|
+
collector,
|
|
292
|
+
code: "content_model_error",
|
|
293
|
+
message: "Content of element '#{xml_element.name}' is not " \
|
|
294
|
+
"valid against its content model",
|
|
295
|
+
location: xml_element.xpath,
|
|
296
|
+
)
|
|
297
|
+
end
|
|
269
298
|
|
|
270
|
-
|
|
299
|
+
:handled
|
|
300
|
+
end
|
|
271
301
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
302
|
+
# Resolve type from element
|
|
303
|
+
#
|
|
304
|
+
# @param element [Lutaml::Xsd::Element] Schema element
|
|
305
|
+
# @return [Lutaml::Xsd::ComplexType, nil]
|
|
306
|
+
def resolve_type(element, repository = nil)
|
|
307
|
+
return element.complex_type if element.complex_type
|
|
308
|
+
return nil unless element.type
|
|
275
309
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
end
|
|
310
|
+
resolved = repository&.resolve_type(element)
|
|
311
|
+
resolved if resolved.is_a?(Lutaml::Xml::Schema::Xsd::ComplexType)
|
|
312
|
+
end
|
|
280
313
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
314
|
+
# Whether a wildcard's namespace constraint admits the child.
|
|
315
|
+
#
|
|
316
|
+
# @param wildcard [Lutaml::Xsd::Any]
|
|
317
|
+
# @param parent [XmlElement] Content-model owner
|
|
318
|
+
# @param child [XmlElement]
|
|
319
|
+
# @param repository [SchemaDefinitionResolver, nil]
|
|
320
|
+
# @return [Boolean]
|
|
321
|
+
def wildcard_permits?(wildcard, parent, child, repository)
|
|
322
|
+
child_ns = child.namespace_uri.to_s
|
|
323
|
+
case wildcard.namespace.to_s
|
|
324
|
+
when "##any", ""
|
|
325
|
+
true
|
|
326
|
+
when "##other"
|
|
327
|
+
target = content_model_target_namespace(parent, repository)
|
|
328
|
+
!child_ns.empty? && child_ns != target.to_s
|
|
329
|
+
when "##local"
|
|
330
|
+
child_ns.empty?
|
|
331
|
+
when "##targetNamespace"
|
|
332
|
+
child_ns == content_model_target_namespace(parent, repository).to_s
|
|
333
|
+
else
|
|
334
|
+
# space-separated namespace list (##targetNamespace ##local allowed)
|
|
335
|
+
wildcard.namespace.to_s.split(/\s+/).any? do |entry|
|
|
336
|
+
case entry
|
|
337
|
+
when "##local" then child_ns.empty?
|
|
338
|
+
when "##targetNamespace"
|
|
339
|
+
child_ns == content_model_target_namespace(parent,
|
|
340
|
+
repository).to_s
|
|
341
|
+
else entry == child_ns
|
|
342
|
+
end
|
|
296
343
|
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
297
346
|
|
|
298
|
-
|
|
299
|
-
|
|
347
|
+
# @return [Boolean]
|
|
348
|
+
def wildcard_child_declared?(child, repository)
|
|
349
|
+
return true if repository.nil?
|
|
300
350
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
351
|
+
!repository.resolve(child.name, child.namespace_uri).nil?
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# processContents="strict" on a wildcard demands the matched
|
|
355
|
+
# child be declared; lax/skip place no declaration requirement.
|
|
356
|
+
#
|
|
357
|
+
# @return [void]
|
|
358
|
+
def check_strict_wildcard_child(child, wildcard, collector,
|
|
359
|
+
repository)
|
|
360
|
+
return unless wildcard.process_contents.to_s == "strict"
|
|
361
|
+
return if wildcard_child_declared?(child, repository)
|
|
362
|
+
|
|
363
|
+
report_error(
|
|
364
|
+
collector,
|
|
365
|
+
code: "wildcard_undeclared_element",
|
|
366
|
+
message: "Element '#{child.qualified_name}' matches " \
|
|
367
|
+
"xs:any with processContents=strict but is not " \
|
|
368
|
+
"declared",
|
|
369
|
+
location: child.xpath,
|
|
370
|
+
context: { element: child.qualified_name },
|
|
371
|
+
)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Best-effort target namespace of the schema owning the parent's
|
|
375
|
+
# declaration (via the resolver index).
|
|
376
|
+
#
|
|
377
|
+
# @return [String, nil]
|
|
378
|
+
def content_model_target_namespace(parent, repository)
|
|
379
|
+
return nil unless repository
|
|
380
|
+
|
|
381
|
+
declaration = repository.resolve(parent.name, parent.namespace_uri)
|
|
382
|
+
repository.target_namespace_for(declaration) if declaration
|
|
315
383
|
end
|
|
316
384
|
|
|
317
385
|
# Collect particles from content model
|
|
@@ -320,11 +388,21 @@ module Lutaml
|
|
|
320
388
|
# Lutaml::Xsd::All]
|
|
321
389
|
# @return [Array]
|
|
322
390
|
def collect_particles(content_model)
|
|
391
|
+
# resolved_element_order preserves declaration order including
|
|
392
|
+
# xs:any particles (which child_elements drops).
|
|
393
|
+
if content_model.respond_to?(:resolved_element_order) &&
|
|
394
|
+
content_model.resolved_element_order.to_a.any?
|
|
395
|
+
return content_model.resolved_element_order.to_a
|
|
396
|
+
end
|
|
397
|
+
|
|
323
398
|
particles = []
|
|
324
399
|
|
|
325
400
|
# Collect elements
|
|
326
401
|
particles.concat(Array(content_model.element)) if content_model.element.any?
|
|
327
402
|
|
|
403
|
+
# Collect wildcards
|
|
404
|
+
particles.concat(Array(content_model.any)) if content_model.any
|
|
405
|
+
|
|
328
406
|
# Collect nested sequences
|
|
329
407
|
particles.concat(Array(content_model.sequence)) if content_model.sequence
|
|
330
408
|
|
|
@@ -341,38 +419,78 @@ module Lutaml
|
|
|
341
419
|
#
|
|
342
420
|
# @param xml_element [XmlElement] The XML element
|
|
343
421
|
# @param particle [Object] The schema particle
|
|
422
|
+
# @param repository [SchemaDefinitionResolver, nil]
|
|
423
|
+
# @param parent [XmlElement, nil] Content-model owner, for
|
|
424
|
+
# wildcard namespace constraints scoped to the target namespace
|
|
344
425
|
# @return [Boolean]
|
|
345
|
-
def particle_matches?(xml_element, particle
|
|
426
|
+
def particle_matches?(xml_element, particle, repository = nil,
|
|
427
|
+
parent: nil)
|
|
346
428
|
case particle
|
|
347
429
|
when Lutaml::Xsd::Element
|
|
348
|
-
element_matches?(xml_element, particle)
|
|
430
|
+
element_matches?(xml_element, particle, repository)
|
|
431
|
+
when Lutaml::Xsd::Any
|
|
432
|
+
wildcard_permits?(particle, parent || xml_element,
|
|
433
|
+
xml_element, repository)
|
|
349
434
|
when Lutaml::Xsd::Sequence, Lutaml::Xsd::Choice, Lutaml::Xsd::All
|
|
350
|
-
# Nested content model
|
|
351
|
-
|
|
435
|
+
# Nested content model: a child matches the container when it
|
|
436
|
+
# matches any leaf particle inside it. Inner ordering is
|
|
437
|
+
# permissive; the outer walk stays sequential.
|
|
438
|
+
collect_particles(particle).any? do |leaf|
|
|
439
|
+
particle_matches?(xml_element, leaf, repository,
|
|
440
|
+
parent: parent)
|
|
441
|
+
end
|
|
352
442
|
else
|
|
353
443
|
false
|
|
354
444
|
end
|
|
355
445
|
end
|
|
356
446
|
|
|
357
|
-
# Check if XML element matches schema element
|
|
447
|
+
# Check if XML element matches schema element.
|
|
448
|
+
#
|
|
449
|
+
# A substitution-group member matches a reference to its head.
|
|
358
450
|
#
|
|
359
451
|
# @param xml_element [XmlElement] The XML element
|
|
360
452
|
# @param schema_element [Lutaml::Xsd::Element] The schema element
|
|
453
|
+
# @param repository [SchemaDefinitionResolver, nil]
|
|
361
454
|
# @return [Boolean]
|
|
362
|
-
def element_matches?(xml_element, schema_element)
|
|
363
|
-
|
|
455
|
+
def element_matches?(xml_element, schema_element, repository = nil)
|
|
456
|
+
if xml_element.name == schema_element.name
|
|
457
|
+
return namespaces_match_instance?(
|
|
458
|
+
xml_element,
|
|
459
|
+
resolve_target_namespace(schema_element, repository),
|
|
460
|
+
)
|
|
461
|
+
end
|
|
364
462
|
|
|
365
|
-
|
|
366
|
-
|
|
463
|
+
substitution_member?(xml_element, schema_element, repository)
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Whether the instance element's declaration is in the
|
|
467
|
+
# substitution group headed by the particle's name/ref.
|
|
468
|
+
#
|
|
469
|
+
# @param xml_element [XmlElement]
|
|
470
|
+
# @param particle [Lutaml::Xsd::Element]
|
|
471
|
+
# @param repository [SchemaDefinitionResolver, nil]
|
|
472
|
+
# @return [Boolean]
|
|
473
|
+
def substitution_member?(xml_element, particle, repository)
|
|
474
|
+
return false unless repository
|
|
475
|
+
|
|
476
|
+
expected = particle.name || particle.ref
|
|
477
|
+
return false if expected.to_s.empty?
|
|
478
|
+
|
|
479
|
+
declaration = repository.resolve(
|
|
480
|
+
xml_element.name, xml_element.namespace_uri
|
|
481
|
+
)
|
|
482
|
+
return false unless declaration
|
|
483
|
+
|
|
484
|
+
declaration.substitution_group.to_s.split(":").last ==
|
|
485
|
+
expected.to_s.split(":").last
|
|
486
|
+
end
|
|
367
487
|
|
|
368
|
-
|
|
488
|
+
# @return [Boolean]
|
|
489
|
+
def namespaces_match_instance?(xml_element, target_ns)
|
|
369
490
|
xml_ns = xml_element.namespace_uri
|
|
370
491
|
xml_ns = nil if xml_ns.to_s.empty?
|
|
371
492
|
target_ns = nil if target_ns.to_s.empty?
|
|
372
493
|
|
|
373
|
-
# For inline elements with no explicit namespace, match if both are nil
|
|
374
|
-
# This handles the common case where elements are defined without
|
|
375
|
-
# a target namespace in the schema and XML has no namespace declarations
|
|
376
494
|
xml_ns == target_ns
|
|
377
495
|
end
|
|
378
496
|
|
|
@@ -411,21 +529,13 @@ module Lutaml
|
|
|
411
529
|
#
|
|
412
530
|
# @param schema_element [Lutaml::Xsd::Element] Schema element
|
|
413
531
|
# @return [String, nil]
|
|
414
|
-
def resolve_target_namespace(schema_element)
|
|
415
|
-
|
|
416
|
-
|
|
532
|
+
def resolve_target_namespace(schema_element, repository = nil)
|
|
533
|
+
# Inline declarations carry no namespace of their own; the
|
|
534
|
+
# resolver knows the owning schema's target namespace.
|
|
535
|
+
from_index = repository&.target_namespace_for(schema_element)
|
|
536
|
+
return from_index unless from_index.nil?
|
|
417
537
|
|
|
418
|
-
|
|
419
|
-
#
|
|
420
|
-
# @param ns1 [String, nil] First namespace
|
|
421
|
-
# @param ns2 [String, nil] Second namespace
|
|
422
|
-
# @return [Boolean]
|
|
423
|
-
def namespaces_match?(ns1, ns2)
|
|
424
|
-
# Normalize empty strings to nil
|
|
425
|
-
normalized_ns1 = ns1.to_s.empty? ? nil : ns1
|
|
426
|
-
normalized_ns2 = ns2.to_s.empty? ? nil : ns2
|
|
427
|
-
|
|
428
|
-
normalized_ns1 == normalized_ns2
|
|
538
|
+
schema_element.target_namespace
|
|
429
539
|
end
|
|
430
540
|
end
|
|
431
541
|
end
|