lutaml-xsd 1.2.79 → 1.2.81
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/TODO.conformance.md +70 -8
- data/lib/lutaml/xsd/validation/content_model_matcher.rb +22 -8
- data/lib/lutaml/xsd/validation/rules/content_model_validation_rule.rb +124 -23
- data/lib/lutaml/xsd/validation/schema_definition_resolver.rb +44 -1
- data/lib/lutaml/xsd/validation/validation_rule.rb +28 -9
- data/lib/lutaml/xsd/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 467b4c2343707af1d200289a1666d12e3ab984c438f0a29ef601d92143914562
|
|
4
|
+
data.tar.gz: d249de5b32c7fb201c8025cf56af088bde07acb6e1f76d2cd3ae1f15bd39cf29
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f9c9c9c71bf3e064dd93ab6e007df3f9d90aaad2478bf77d8f7b7915804b5e8218a6d8adb97be02293560dc261ed957a6fe09af5404a30b18ee7a0d5d1a941e
|
|
7
|
+
data.tar.gz: 539a1503358510cde20356dc3d6a0a1a2c0b9daaebf77bda9ef45bc54f45222a0eaf2d97477b05f1347b1d35dcc22742963b0fc388823217bc6cd45ceb900bdf
|
data/TODO.conformance.md
CHANGED
|
@@ -120,22 +120,29 @@ pinned commit) is measured via `rake conformance:w3c`:
|
|
|
120
120
|
|
|
121
121
|
| mode | pass-rate |
|
|
122
122
|
|---|---|
|
|
123
|
-
| pipeline |
|
|
123
|
+
| pipeline | 4573/4696 (97.38%) |
|
|
124
124
|
|
|
125
|
-
(2026-11-02, bundled-stack measurement
|
|
125
|
+
(2026-11-02, bundled-stack measurement with the escape,
|
|
126
126
|
hint-following, pattern-alternative, identity-crash, engine-set,
|
|
127
127
|
engine-refresh, IC value-space, IC field-resolution, facade-silence,
|
|
128
128
|
regex-semantics, redefine-refresh, gmonth-refresh, digit-vintage,
|
|
129
|
-
stgh-refresh, group-ref, attribute-group-redefine
|
|
130
|
-
notation-length rounds; moxml
|
|
131
|
-
0.6.
|
|
132
|
-
the Opal surface
|
|
133
|
-
0.8.
|
|
129
|
+
stgh-refresh, group-ref, attribute-group-redefine,
|
|
130
|
+
notation-length and ref-namespace rounds; moxml
|
|
131
|
+
0.6.2 (gemspec floor `~> 0.6.0` per the owner — the 0.6 line drops
|
|
132
|
+
the Opal surface), lutaml-model
|
|
133
|
+
0.8.157 (gemspec floor `~> 0.8.156` per the owner; the lockfile is
|
|
134
|
+
no longer committed — measurements resolve fresh), engine leptris
|
|
135
|
+
1.9.402.0. The 1.9.402/0.6.2/0.8.157 ride costs 8 net: engine
|
|
136
|
+
fixed+EMPTY content regressions (9 cases, filed leptris-rs#25 —
|
|
137
|
+
empty content satisfies the fixed facet per §3.12.2/§4.3.1.2),
|
|
138
|
+
reU6 we-accept, MINUS two drift fixes (RegexTest_234, stE063).)
|
|
139
|
+
Same-stack comparison: 1.2.43
|
|
134
140
|
baseline 3961 (84.3%) → 4039 (86.0%) → 4090 (87.1%) → 4360 (92.8%,
|
|
135
141
|
lockfile refresh) → 4411 (93.9%) → 4430 (94.3%) → 4435 (94.4%) →
|
|
136
142
|
4436 (94.5%) → 4439 (94.5%) → 4454 (94.9%) → 4495 (95.7%) → 4524
|
|
137
143
|
(96.3%) → 4534 (96.6%) → 4540 (96.7%) → 4542 (96.7%) → 4546 (96.8%)
|
|
138
|
-
→ 4557 (97.0%) → 4565 (97.2%) → 4569 (97.3%)
|
|
144
|
+
→ 4557 (97.0%) → 4565 (97.2%) → 4569 (97.3%) → 4562 (drift raw) →
|
|
145
|
+
4563 (97.17%, ref-namespace round) → 4573 (97.38%, group-content round), +612 net over the 1.2.43 baseline. The escapes round (PR #197 — \d→\p{Nd},
|
|
139
146
|
\w/\W/\s to their XSD value spaces) collapses Regex 120→69;
|
|
140
147
|
same-step pattern facets now behave as alternatives and
|
|
141
148
|
whitespace-only text reaches the facets intact (PR #200), Regex
|
|
@@ -230,6 +237,8 @@ corners.
|
|
|
230
237
|
| Built-in lexical tables reject valid forms (char-reference shapes; gMonth FIXED 1.9.398, \d legacy-Nd FIXED 1.9.399, stG/stH list lexicals FIXED 1.9.400/401) | ~5 | leptris#1936 |
|
|
231
238
|
| Automaton size cap surfaces as an instance error instead of engine-inert silence | 0 — FIXED in leptris 1.9.398 | moxml#514 (follow-up to #473) |
|
|
232
239
|
| Regex dialect remainder (engine `\d` vintage, `\p{Is...}` blocks, char-reference shapes) | ~30 | engine-side of leptris#1936 |
|
|
240
|
+
| Empty content against a fixed element value rejected (1.9.402.0 regression — isDefault001/002/076, ctZ013a, elemQ019, stE065/066/096, particlesZ040) | 9 | leptris-rs#25 |
|
|
241
|
+
| Unresolvable include/import/redefine schemaLocation raises at parse instead of warning (anyURI_a001/a002, schD7, schG8) | 4 | lutaml-model#1087 |
|
|
233
242
|
| Strict wildcard/attribute admission on further compositions | ~5 | moxml#510 thread |
|
|
234
243
|
| Ruby-side under-detections (Group 16, DataTypes 7, IC 6, Attribute 5, Additional 4, Particles 4, SimpleType 4, Element 1, Wildcards 1 — minus what the regex round banked) | ~60 | OURS — the remaining Ruby work queue |
|
|
235
244
|
|
|
@@ -388,6 +397,59 @@ tuple keys, node-set field violations and absent-field rows handled, cross-eleme
|
|
|
388
397
|
duration/dateTime/gMonthDay/gYear corners (engine-bound and
|
|
389
398
|
contested-highlight), gMonth_pattern001 (bugzilla 6901 — do not
|
|
390
399
|
chase), double018 (disputed reading).
|
|
400
|
+
- REF-NAMESPACE ROUND (2026-11-02): +2 Particles (13→11, particlesDb001/
|
|
401
|
+
Db006). A ref particle names a declaration in ONE namespace, but the
|
|
402
|
+
match path resolved it against the CHILD's namespace first — a global
|
|
403
|
+
declaration duplicated across an imported namespace satisfied the ref
|
|
404
|
+
and foreign substitutes were accepted. The ref branch now prefers the
|
|
405
|
+
declaration in the content-model OWNER schema's target namespace
|
|
406
|
+
(threaded through the match chain), then the child's, then the
|
|
407
|
+
unique-name fallback; parsed particles carry no prefix map
|
|
408
|
+
(lutaml-model discards xmlns), so the owner namespace is the
|
|
409
|
+
strongest signal available. The engine — fed documents one at a
|
|
410
|
+
time — admits the substitute, so a targeted scan on the engine-accept
|
|
411
|
+
path reports the foreign child (scan_cross_namespace_refs). NIL
|
|
412
|
+
owner namespace means UNKNOWN, not "no namespace": resolve(local,
|
|
413
|
+
nil) only matches no-namespace declarations and a hint-loaded
|
|
414
|
+
document can supply a foreign namesake (idH021 regression caught
|
|
415
|
+
and fixed in-round). Zero regressions on the drifted stack.
|
|
416
|
+
- STACK DRIFT (2026-11-02, same day): the lockfile is no longer
|
|
417
|
+
committed (owner chore) — measurements resolve fresh and rode
|
|
418
|
+
moxml 0.6.1→0.6.2, leptris 1.9.401.0→1.9.402.0, lutaml-model
|
|
419
|
+
0.8.155→0.8.157 at once. TWO fixes rode in (RegexTest_234,
|
|
420
|
+
stE063); the fixed+EMPTY engine regression cost 9 (leptris-rs#25)
|
|
421
|
+
and reU6 flipped to we-accept (regex class, untriaged). Suite
|
|
422
|
+
4569→4562 raw; 4563 with this round. NET -6 documented, not
|
|
423
|
+
chased — the fixed-empty leg reverses the moment leptris-rs#25
|
|
424
|
+
lands.
|
|
425
|
+
- GROUP-CONTENT ROUND (2026-11-02): +13 (Group 8→0 — the family is
|
|
426
|
+
CLEAN at 126/126; Particles Db001/Db006 +2, Ea004/011/018 +3; suite
|
|
427
|
+
4563→4573 = 97.38% on the drifted stack, rspec green). FOUR Ruby
|
|
428
|
+
defects: (a) resolve_group scanned only top-level schemas — a group
|
|
429
|
+
declared in an IMPORTED document (the extension-body shape)
|
|
430
|
+
resolved to nothing (groupB009/010); nested resolved schemas are
|
|
431
|
+
now searched depth-guarded. (b) The NFA wiring missed the
|
|
432
|
+
lutaml-model container classes: a group whose declaration is xs:all
|
|
433
|
+
compiled ORDERED (groupO004), and with_occurrence gave min=0
|
|
434
|
+
particles one phantom extra copy (min=0/max=1 compiled capacity 2 —
|
|
435
|
+
masked for bare elements by the occurrence rule, visible wherever
|
|
436
|
+
nested group bounds lean on this verdict: groupJ003/J006/J008/L008
|
|
437
|
+
accepted forbidden repeats); a max=0 particle compiles to the empty
|
|
438
|
+
fragment (groupH008). (c) type_particles had no complexContent
|
|
439
|
+
restriction branch — a restriction body compiled EMPTY and children
|
|
440
|
+
passed unchecked (groupH008); a restriction replaces the base
|
|
441
|
+
content model. (d) The ref owner-namespace probe resolved ANY
|
|
442
|
+
declaration — including inline ones — so a qualified local namesake
|
|
443
|
+
hijacked unprefixed refs; the probe is GLOBAL-only (resolve_global),
|
|
444
|
+
the unprefixed ref canonically names a no-namespace declaration,
|
|
445
|
+
several distinct prefixed forms of one local name mean deliberate
|
|
446
|
+
disambiguation (no owner guess), and an unresolvable ref matches by
|
|
447
|
+
local name alone. KNOWN GAP left open: particlesQ032 — a
|
|
448
|
+
no-namespace document imported under an EXPLICIT namespace: its
|
|
449
|
+
globals must adopt the import namespace (MS-lenient chameleon
|
|
450
|
+
import; our index keeps them no-namespace so the four e2 refs
|
|
451
|
+
mis-resolve). Zero regressions; the drift entries above remain
|
|
452
|
+
(fixed-empty 9, reU6).
|
|
391
453
|
- REGEX SEMANTICS ROUND (2026-10-31, PR #208): \w/\W now carry their
|
|
392
454
|
Appendix F complements (\w = [^\p{P}\p{Z}\p{C}], class interiors
|
|
393
455
|
via the exact positive partition L/M/N/S), \I/\C complements added
|
|
@@ -134,13 +134,13 @@ module Lutaml
|
|
|
134
134
|
|
|
135
135
|
def compile_particle(particle)
|
|
136
136
|
case particle
|
|
137
|
-
when Lutaml::Xsd::Sequence
|
|
137
|
+
when Lutaml::Xsd::Sequence, Lutaml::Xml::Schema::Xsd::Sequence
|
|
138
138
|
compile_bounded_container(particle)
|
|
139
|
-
when Lutaml::Xsd::Choice
|
|
139
|
+
when Lutaml::Xsd::Choice, Lutaml::Xml::Schema::Xsd::Choice
|
|
140
140
|
with_occurrence(particle) do
|
|
141
141
|
compile_choice_particles(@container_particles.call(particle))
|
|
142
142
|
end
|
|
143
|
-
when Lutaml::Xsd::All
|
|
143
|
+
when Lutaml::Xsd::All, Lutaml::Xml::Schema::Xsd::All
|
|
144
144
|
# The all's own bounds wrap the member bitmask: an optional
|
|
145
145
|
# all accepts the empty sequence even with mandatory
|
|
146
146
|
# members (addB31).
|
|
@@ -181,10 +181,17 @@ module Lutaml
|
|
|
181
181
|
end
|
|
182
182
|
|
|
183
183
|
# A group ref expands to its declaration's container, whose own
|
|
184
|
-
# kind (sequence vs choice) decides the wiring.
|
|
184
|
+
# kind (sequence vs choice vs all) decides the wiring. Imported
|
|
185
|
+
# documents parse to the lutaml-model container classes —
|
|
186
|
+
# groupO004's xs:all compiled ORDERED before both All classes
|
|
187
|
+
# were wired to the bitmask.
|
|
185
188
|
def compile_container_body(container)
|
|
186
|
-
if container.is_a?(Lutaml::Xsd::Choice)
|
|
189
|
+
if container.is_a?(Lutaml::Xsd::Choice) ||
|
|
190
|
+
container.is_a?(Lutaml::Xml::Schema::Xsd::Choice)
|
|
187
191
|
compile_choice_particles(@container_particles.call(container))
|
|
192
|
+
elsif container.is_a?(Lutaml::Xsd::All) ||
|
|
193
|
+
container.is_a?(Lutaml::Xml::Schema::Xsd::All)
|
|
194
|
+
with_occurrence(container) { compile_all(container) }
|
|
188
195
|
else
|
|
189
196
|
compile_particles(@container_particles.call(container))
|
|
190
197
|
end
|
|
@@ -235,14 +242,20 @@ module Lutaml
|
|
|
235
242
|
|
|
236
243
|
# Wraps a body fragment with the particle's occurrence range:
|
|
237
244
|
# min mandatory copies, then an unbounded loop on one extra copy
|
|
238
|
-
# or (max - min) optional copies.
|
|
245
|
+
# or (max - min) optional copies. The first body copy counts
|
|
246
|
+
# toward the bound: with min=0 it IS the first optional copy, so
|
|
247
|
+
# only max-1 more are wired (min=0/max=1 used to compile with
|
|
248
|
+
# capacity 2 — masked for bare elements by the occurrence rule,
|
|
249
|
+
# visible wherever nested group bounds lean on this verdict).
|
|
239
250
|
def with_occurrence(particle)
|
|
240
251
|
min = parse_occurs(particle.min_occurs, 1)
|
|
241
252
|
max = parse_max_occurs(particle.max_occurs, 1)
|
|
242
|
-
|
|
253
|
+
return empty_fragment if max.is_a?(Integer) && max.zero?
|
|
254
|
+
|
|
243
255
|
entry, exit_state = yield
|
|
244
256
|
cursor = exit_state
|
|
245
257
|
last_entry = entry
|
|
258
|
+
eff_min = [min, MAX_UNROLL].min
|
|
246
259
|
(eff_min - 1).times do
|
|
247
260
|
c_entry, c_exit = yield
|
|
248
261
|
@epsilons[cursor] << c_entry
|
|
@@ -259,7 +272,8 @@ module Lutaml
|
|
|
259
272
|
@epsilons[exit_state] << final
|
|
260
273
|
else
|
|
261
274
|
@epsilons[cursor] << final
|
|
262
|
-
|
|
275
|
+
optional = max - min - (min.zero? && max.is_a?(Integer) ? 1 : 0)
|
|
276
|
+
optional.times do
|
|
263
277
|
c_entry, c_exit = yield
|
|
264
278
|
@epsilons[cursor] << c_entry
|
|
265
279
|
@epsilons[c_exit] << final
|
|
@@ -94,8 +94,9 @@ module Lutaml
|
|
|
94
94
|
|
|
95
95
|
# The engine does not check abstract-element usage; scan for
|
|
96
96
|
# it regardless of whether the engine answered.
|
|
97
|
+
owner_ns = repository&.target_namespace_for(schema_def)
|
|
97
98
|
scan_abstract_usage(xml_element, particles, collector,
|
|
98
|
-
repository)
|
|
99
|
+
repository, owner_ns)
|
|
99
100
|
|
|
100
101
|
handled = if occurrence_guard(repository).particles_safe?(particles)
|
|
101
102
|
engine_content_check(xml_element,
|
|
@@ -107,10 +108,14 @@ module Lutaml
|
|
|
107
108
|
# (particlesZ034_b); the Ruby matcher decides those.
|
|
108
109
|
false
|
|
109
110
|
end
|
|
110
|
-
|
|
111
|
+
if handled
|
|
112
|
+
scan_cross_namespace_refs(xml_element, particles, collector,
|
|
113
|
+
repository, owner_ns)
|
|
114
|
+
return
|
|
115
|
+
end
|
|
111
116
|
|
|
112
117
|
validate_particle_sequence(xml_element, particles, collector,
|
|
113
|
-
repository)
|
|
118
|
+
repository, owner_ns)
|
|
114
119
|
end
|
|
115
120
|
|
|
116
121
|
private
|
|
@@ -133,14 +138,14 @@ module Lutaml
|
|
|
133
138
|
#
|
|
134
139
|
# @return [void]
|
|
135
140
|
def validate_particle_sequence(xml_element, particles, collector,
|
|
136
|
-
repository)
|
|
141
|
+
repository, owner_ns = nil)
|
|
137
142
|
return if particles.empty?
|
|
138
143
|
|
|
139
144
|
children = Array(xml_element.children)
|
|
140
145
|
matcher = ContentModelMatcher.new(
|
|
141
146
|
particles,
|
|
142
147
|
element_match: ->(child, particle) do
|
|
143
|
-
element_matches?(child, particle, repository)
|
|
148
|
+
element_matches?(child, particle, repository, owner_ns)
|
|
144
149
|
end,
|
|
145
150
|
wildcard_match: ->(child, wildcard) do
|
|
146
151
|
wildcard_permits?(wildcard, xml_element, child, repository)
|
|
@@ -155,13 +160,13 @@ module Lutaml
|
|
|
155
160
|
result = matcher.match(children)
|
|
156
161
|
if result.matched
|
|
157
162
|
scan_wildcard_strict_usage(xml_element, particles, collector,
|
|
158
|
-
repository)
|
|
163
|
+
repository, owner_ns)
|
|
159
164
|
return
|
|
160
165
|
end
|
|
161
166
|
|
|
162
167
|
known = collector.errors.size
|
|
163
168
|
walk_sequence_diagnostics(xml_element, particles, collector,
|
|
164
|
-
repository)
|
|
169
|
+
repository, owner_ns)
|
|
165
170
|
return if collector.errors.size > known
|
|
166
171
|
|
|
167
172
|
report_matcher_failure(result, children, xml_element, collector)
|
|
@@ -294,6 +299,7 @@ module Lutaml
|
|
|
294
299
|
type.complex_content
|
|
295
300
|
content = type.complex_content
|
|
296
301
|
extension = Array(content.extension).first
|
|
302
|
+
restriction = Array(content.restriction).first
|
|
297
303
|
if extension
|
|
298
304
|
base_local = extension.base.to_s.split(":").last
|
|
299
305
|
base_type = base_local && repository&.resolve_named_type(base_local)
|
|
@@ -306,6 +312,14 @@ module Lutaml
|
|
|
306
312
|
particles << particle
|
|
307
313
|
end
|
|
308
314
|
end
|
|
315
|
+
elsif restriction
|
|
316
|
+
# A restriction REPLACES the base content model: only
|
|
317
|
+
# the restriction body's particles apply (groupH008's
|
|
318
|
+
# prohibited group ref was previously compiled as an
|
|
319
|
+
# EMPTY model and the children passed unchecked).
|
|
320
|
+
particles = COMPLEX_PARTICLE_KINDS.flat_map do |kind|
|
|
321
|
+
Array(restriction.public_send(kind)).compact
|
|
322
|
+
end
|
|
309
323
|
end
|
|
310
324
|
end
|
|
311
325
|
|
|
@@ -317,15 +331,16 @@ module Lutaml
|
|
|
317
331
|
# instances. Containers flatten to their leaf declarations —
|
|
318
332
|
# the walk keeps them whole, the scan needs the leaves.
|
|
319
333
|
def scan_abstract_usage(xml_element, particles, collector,
|
|
320
|
-
repository)
|
|
334
|
+
repository, owner_ns = nil)
|
|
321
335
|
leaves = leaf_particles(particles, repository)
|
|
322
336
|
xml_element.children.each do |child|
|
|
323
337
|
leaves.each do |particle|
|
|
324
338
|
next unless particle.is_a?(Lutaml::Xsd::Element)
|
|
325
339
|
next unless particle_matches?(child, particle, repository,
|
|
326
|
-
parent: xml_element
|
|
340
|
+
parent: xml_element,
|
|
341
|
+
owner_ns: owner_ns)
|
|
327
342
|
next unless element_declaration_abstract?(particle, child,
|
|
328
|
-
repository)
|
|
343
|
+
repository, owner_ns)
|
|
329
344
|
|
|
330
345
|
report_error(
|
|
331
346
|
collector,
|
|
@@ -360,7 +375,7 @@ module Lutaml
|
|
|
360
375
|
#
|
|
361
376
|
# @return [void]
|
|
362
377
|
def scan_wildcard_strict_usage(xml_element, particles, collector,
|
|
363
|
-
repository)
|
|
378
|
+
repository, owner_ns = nil)
|
|
364
379
|
leaves = leaf_particles(particles, repository)
|
|
365
380
|
wildcards = leaves.select { |p| p.is_a?(Lutaml::Xsd::Any) }
|
|
366
381
|
return if wildcards.empty?
|
|
@@ -370,7 +385,7 @@ module Lutaml
|
|
|
370
385
|
end
|
|
371
386
|
xml_element.children.each do |child|
|
|
372
387
|
next if element_leaves.any? do |p|
|
|
373
|
-
element_matches?(child, p, repository)
|
|
388
|
+
element_matches?(child, p, repository, owner_ns)
|
|
374
389
|
end
|
|
375
390
|
|
|
376
391
|
wildcards.each do |wildcard|
|
|
@@ -391,7 +406,7 @@ module Lutaml
|
|
|
391
406
|
#
|
|
392
407
|
# @return [void]
|
|
393
408
|
def walk_sequence_diagnostics(xml_element, particles, collector,
|
|
394
|
-
|
|
409
|
+
repository, owner_ns = nil)
|
|
395
410
|
return if particles.empty?
|
|
396
411
|
|
|
397
412
|
children = xml_element.children
|
|
@@ -418,13 +433,15 @@ module Lutaml
|
|
|
418
433
|
child = children[child_index]
|
|
419
434
|
|
|
420
435
|
break unless particle_matches?(child, particle, repository,
|
|
421
|
-
parent: xml_element
|
|
436
|
+
parent: xml_element,
|
|
437
|
+
owner_ns: owner_ns)
|
|
422
438
|
|
|
423
439
|
if particle.is_a?(Lutaml::Xsd::Any)
|
|
424
440
|
check_strict_wildcard_child(child, particle, collector,
|
|
425
441
|
repository)
|
|
426
442
|
elsif particle.is_a?(Lutaml::Xsd::Element) &&
|
|
427
|
-
element_declaration_abstract?(particle, child, repository
|
|
443
|
+
element_declaration_abstract?(particle, child, repository,
|
|
444
|
+
owner_ns)
|
|
428
445
|
report_error(
|
|
429
446
|
collector,
|
|
430
447
|
code: "abstract_element_used",
|
|
@@ -489,19 +506,101 @@ module Lutaml
|
|
|
489
506
|
# declarations cannot appear directly in instances (substitution
|
|
490
507
|
# members remain legal). Direct declarations carry their own
|
|
491
508
|
# abstract flag; references resolve to the global declaration.
|
|
492
|
-
def element_declaration_abstract?(particle, child, repository
|
|
493
|
-
|
|
509
|
+
def element_declaration_abstract?(particle, child, repository,
|
|
510
|
+
owner_ns = nil)
|
|
511
|
+
declaration = declaration_for(particle, child, repository,
|
|
512
|
+
owner_ns)
|
|
494
513
|
return false unless declaration
|
|
495
514
|
|
|
496
515
|
truthy?(declaration.abstract) && child.name == declaration.name
|
|
497
516
|
end
|
|
498
517
|
|
|
499
|
-
|
|
518
|
+
# A qualified ref names a declaration in ONE namespace.
|
|
519
|
+
# Global names duplicated across namespaces (import + main)
|
|
520
|
+
# resolve against the child's namespace today; a foreign
|
|
521
|
+
# child is an unexpected element (the engine — fed the
|
|
522
|
+
# documents one at a time — admits the substitute, so this
|
|
523
|
+
# runs on the engine-accept path). Wildcard territory is
|
|
524
|
+
# exempt. The PREFIX is the signal: an unprefixed ref
|
|
525
|
+
# canonically names a NO-NAMESPACE declaration (Q016 — its
|
|
526
|
+
# qualified local namesake is irrelevant), a prefixed one
|
|
527
|
+
# names some namespace: a child in the opposite territory,
|
|
528
|
+
# or — for the duplicated-name shape — in any namespace but
|
|
529
|
+
# the owner's, cannot match.
|
|
530
|
+
#
|
|
531
|
+
# @return [void]
|
|
532
|
+
def scan_cross_namespace_refs(xml_element, particles, collector,
|
|
533
|
+
repository, owner_ns)
|
|
534
|
+
return if owner_ns.nil?
|
|
535
|
+
|
|
536
|
+
refs = leaf_particles(particles, repository).select do |p|
|
|
537
|
+
p.is_a?(Lutaml::Xml::Schema::Xsd::Element) &&
|
|
538
|
+
p.ref.to_s.match?(/\S/)
|
|
539
|
+
end
|
|
540
|
+
return if refs.empty?
|
|
541
|
+
|
|
542
|
+
# Several DISTINCT prefixed forms of one local name mean the
|
|
543
|
+
# schema disambiguates by prefix on purpose (particlesQ032:
|
|
544
|
+
# foo:e2 / x:e2 / bar:e2) — the owner-namespace guess is
|
|
545
|
+
# untenable there.
|
|
546
|
+
single_prefixed_form = refs.select do |r|
|
|
547
|
+
r.ref.to_s.include?(":")
|
|
548
|
+
end.group_by { |r| r.ref.to_s.split(":").last }
|
|
549
|
+
.select { |_, group| group.map { |r| r.ref.to_s.split(":").first }.uniq.size == 1 }
|
|
550
|
+
.map(&:first)
|
|
551
|
+
|
|
552
|
+
xml_element.children.each do |child|
|
|
553
|
+
child_ns = child.namespace_uri.to_s
|
|
554
|
+
next if child_ns == owner_ns
|
|
555
|
+
|
|
556
|
+
hit = refs.find do |r|
|
|
557
|
+
ref = r.ref.to_s
|
|
558
|
+
next false unless ref.split(":").last == child.name
|
|
559
|
+
|
|
560
|
+
if ref.include?(":")
|
|
561
|
+
next false unless single_prefixed_form.include?(child.name)
|
|
562
|
+
|
|
563
|
+
child_ns.empty? ||
|
|
564
|
+
(child_ns != owner_ns &&
|
|
565
|
+
repository.resolve_global(child.name, owner_ns))
|
|
566
|
+
else
|
|
567
|
+
child_ns.match?(/\S/)
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
next unless hit
|
|
571
|
+
next if wildcard_covers_child?(xml_element, particles, child,
|
|
572
|
+
repository)
|
|
573
|
+
|
|
574
|
+
report_error(
|
|
575
|
+
collector,
|
|
576
|
+
code: "unexpected_element_in_sequence",
|
|
577
|
+
message: "Unexpected element '#{child.qualified_name}' " \
|
|
578
|
+
"in sequence — the reference names the same " \
|
|
579
|
+
"declaration in another namespace",
|
|
580
|
+
location: child.xpath,
|
|
581
|
+
context: { element: child.qualified_name },
|
|
582
|
+
suggestion: "Use the declaration's namespace or rename " \
|
|
583
|
+
"the duplicate",
|
|
584
|
+
)
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def wildcard_covers_child?(xml_element, particles, child,
|
|
589
|
+
repository)
|
|
590
|
+
leaf_particles(particles, repository).any? do |p|
|
|
591
|
+
next false unless p.is_a?(Lutaml::Xsd::Any)
|
|
592
|
+
|
|
593
|
+
wildcard_permits?(p, xml_element, child, repository)
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def declaration_for(particle, child, repository, owner_ns = nil)
|
|
500
598
|
return particle if particle.name.to_s.match?(/\S/)
|
|
501
599
|
return nil unless repository
|
|
502
600
|
|
|
503
601
|
local = particle.ref.to_s.split(":").last
|
|
504
|
-
repository.
|
|
602
|
+
(owner_ns && repository.resolve_global(local, owner_ns)) ||
|
|
603
|
+
repository.resolve(local, child.namespace_uri) ||
|
|
505
604
|
repository.resolve(local, nil)
|
|
506
605
|
end
|
|
507
606
|
|
|
@@ -649,10 +748,11 @@ module Lutaml
|
|
|
649
748
|
# wildcard namespace constraints scoped to the target namespace
|
|
650
749
|
# @return [Boolean]
|
|
651
750
|
def particle_matches?(xml_element, particle, repository = nil,
|
|
652
|
-
parent: nil, group_depth: 0)
|
|
751
|
+
parent: nil, group_depth: 0, owner_ns: nil)
|
|
653
752
|
particle_matches_child?(xml_element, particle, repository,
|
|
654
753
|
parent: parent,
|
|
655
|
-
group_depth: group_depth
|
|
754
|
+
group_depth: group_depth,
|
|
755
|
+
owner_ns: owner_ns)
|
|
656
756
|
end
|
|
657
757
|
|
|
658
758
|
# Check if XML element matches schema element.
|
|
@@ -663,9 +763,10 @@ module Lutaml
|
|
|
663
763
|
# @param schema_element [Lutaml::Xsd::Element] The schema element
|
|
664
764
|
# @param repository [SchemaDefinitionResolver, nil]
|
|
665
765
|
# @return [Boolean]
|
|
666
|
-
def element_matches?(xml_element, schema_element, repository = nil
|
|
766
|
+
def element_matches?(xml_element, schema_element, repository = nil,
|
|
767
|
+
owner_ns = nil)
|
|
667
768
|
particle_matches_declaration?(xml_element, schema_element,
|
|
668
|
-
repository) ||
|
|
769
|
+
repository, owner_ns) ||
|
|
669
770
|
substitution_member?(xml_element, schema_element, repository)
|
|
670
771
|
end
|
|
671
772
|
|
|
@@ -156,6 +156,27 @@ module Lutaml
|
|
|
156
156
|
candidates.length == 1 ? candidates.first.first : nil
|
|
157
157
|
end
|
|
158
158
|
|
|
159
|
+
# Global (top-level) element declaration only. A ref particle
|
|
160
|
+
# names a GLOBAL declaration; the index also holds inline ones,
|
|
161
|
+
# and a qualified local namesake must never satisfy the
|
|
162
|
+
# owner-namespace probe (particlesQ016: ref "foo" unprefixed vs
|
|
163
|
+
# the qualified local foo inside the restricted type).
|
|
164
|
+
#
|
|
165
|
+
# @param local_name [String]
|
|
166
|
+
# @param namespace_uri [String, nil]
|
|
167
|
+
# @return [Lutaml::Xml::Schema::Xsd::Element, nil]
|
|
168
|
+
def resolve_global(local_name, namespace_uri)
|
|
169
|
+
return nil if local_name.nil? || local_name.empty?
|
|
170
|
+
|
|
171
|
+
candidates = @elements[local_name]
|
|
172
|
+
return nil if candidates.empty?
|
|
173
|
+
|
|
174
|
+
exact = candidates.find do |_element, namespace, kind|
|
|
175
|
+
kind == :top && namespace_matches?(namespace, namespace_uri)
|
|
176
|
+
end
|
|
177
|
+
exact&.first
|
|
178
|
+
end
|
|
179
|
+
|
|
159
180
|
# Named type for an element's `type="prefix:Name"` reference.
|
|
160
181
|
#
|
|
161
182
|
# @param schema_element [Lutaml::Xml::Schema::Xsd::Element, nil]
|
|
@@ -386,7 +407,10 @@ module Lutaml
|
|
|
386
407
|
end
|
|
387
408
|
|
|
388
409
|
# Named group declaration by local name (for xs:group reference
|
|
389
|
-
# expansion in the content and occurrence walks).
|
|
410
|
+
# expansion in the content and occurrence walks). Group refs
|
|
411
|
+
# reach across imports (groupB009: an extension body references
|
|
412
|
+
# a group declared in the imported document), so nested
|
|
413
|
+
# resolved schemas are searched too.
|
|
390
414
|
#
|
|
391
415
|
# @param local_name [String]
|
|
392
416
|
# @return [Lutaml::Xml::Schema::Xsd::Group, nil]
|
|
@@ -396,6 +420,25 @@ module Lutaml
|
|
|
396
420
|
schemas.each do |schema|
|
|
397
421
|
found = Array(schema.group).find { |g| g.name == local_name }
|
|
398
422
|
return found if found
|
|
423
|
+
|
|
424
|
+
found = find_nested_group(local_name, schema)
|
|
425
|
+
return found if found
|
|
426
|
+
end
|
|
427
|
+
nil
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# @return [Object, nil]
|
|
431
|
+
def find_nested_group(local_name, schema, depth = 0)
|
|
432
|
+
return nil if depth > 8
|
|
433
|
+
|
|
434
|
+
(Array(schema.include) + Array(schema.import)).each do |nested|
|
|
435
|
+
next unless nested.is_a?(Lutaml::Xml::Schema::Xsd::Schema)
|
|
436
|
+
|
|
437
|
+
found = Array(nested.group).find { |g| g.name == local_name }
|
|
438
|
+
return found if found
|
|
439
|
+
|
|
440
|
+
found = find_nested_group(local_name, nested, depth + 1)
|
|
441
|
+
return found if found
|
|
399
442
|
end
|
|
400
443
|
nil
|
|
401
444
|
end
|
|
@@ -227,14 +227,18 @@ module Lutaml
|
|
|
227
227
|
|
|
228
228
|
# Whether an instance child matches a content-model particle.
|
|
229
229
|
# Direct declarations compare name and expected namespace; ref
|
|
230
|
-
# particles
|
|
231
|
-
#
|
|
230
|
+
# particles name a qualified declaration — the owner schema's
|
|
231
|
+
# target namespace is preferred, then the child's, then the
|
|
232
|
+
# unique-name fallback (parsed particles carry no prefix map).
|
|
232
233
|
#
|
|
233
234
|
# @param child [XmlElement]
|
|
234
235
|
# @param particle [Lutaml::Xsd::Element]
|
|
235
236
|
# @param repository [SchemaDefinitionResolver, nil]
|
|
237
|
+
# @param owner_ns [String, nil] target namespace of the schema
|
|
238
|
+
# owning the content model
|
|
236
239
|
# @return [Boolean]
|
|
237
|
-
def particle_matches_declaration?(child, particle, repository
|
|
240
|
+
def particle_matches_declaration?(child, particle, repository,
|
|
241
|
+
owner_ns = nil)
|
|
238
242
|
ref = particle.ref.to_s
|
|
239
243
|
if ref.empty?
|
|
240
244
|
name = particle.name.to_s
|
|
@@ -245,9 +249,20 @@ module Lutaml
|
|
|
245
249
|
repository))
|
|
246
250
|
else
|
|
247
251
|
local = ref.split(":").last
|
|
248
|
-
|
|
252
|
+
# owner_ns nil means UNKNOWN (the caller has no owner
|
|
253
|
+
# context), not "no namespace" — resolve(local, nil) only
|
|
254
|
+
# matches no-namespace declarations, and a hint-loaded
|
|
255
|
+
# document can supply a foreign namesake. The owner probe
|
|
256
|
+
# is GLOBAL-only: a ref names a global declaration, and an
|
|
257
|
+
# inline namesake must not satisfy it.
|
|
258
|
+
declaration =
|
|
259
|
+
(owner_ns && repository&.resolve_global(local, owner_ns)) ||
|
|
260
|
+
repository&.resolve(local, child.namespace_uri) ||
|
|
249
261
|
repository&.resolve(local, nil)
|
|
250
|
-
|
|
262
|
+
# An unresolvable ref (particlesQ032: a no-namespace document
|
|
263
|
+
# imported under an explicit namespace — its globals index
|
|
264
|
+
# nowhere) is namespace-unconstrained: match by local name.
|
|
265
|
+
return child.name == local unless declaration
|
|
251
266
|
|
|
252
267
|
child.name == declaration.name &&
|
|
253
268
|
namespaces_match?(child.namespace_uri,
|
|
@@ -413,10 +428,12 @@ module Lutaml
|
|
|
413
428
|
# @param group_depth [Integer] recursion guard for group refs
|
|
414
429
|
# @return [Boolean]
|
|
415
430
|
def particle_matches_child?(child, particle, repository,
|
|
416
|
-
parent: nil, group_depth: 0
|
|
431
|
+
parent: nil, group_depth: 0,
|
|
432
|
+
owner_ns: nil)
|
|
417
433
|
case particle
|
|
418
434
|
when Lutaml::Xsd::Element
|
|
419
|
-
particle_matches_declaration?(child, particle, repository
|
|
435
|
+
particle_matches_declaration?(child, particle, repository,
|
|
436
|
+
owner_ns) ||
|
|
420
437
|
substitution_member?(child, particle, repository)
|
|
421
438
|
when Lutaml::Xsd::Any
|
|
422
439
|
wildcard_permits?(particle, parent || child, child, repository)
|
|
@@ -425,13 +442,15 @@ module Lutaml
|
|
|
425
442
|
group_depth).any? do |container|
|
|
426
443
|
particle_matches_child?(child, container, repository,
|
|
427
444
|
parent: parent,
|
|
428
|
-
group_depth: group_depth + 1
|
|
445
|
+
group_depth: group_depth + 1,
|
|
446
|
+
owner_ns: owner_ns)
|
|
429
447
|
end
|
|
430
448
|
when Lutaml::Xsd::Sequence, Lutaml::Xsd::Choice, Lutaml::Xsd::All
|
|
431
449
|
collect_content_particles(particle).any? do |leaf|
|
|
432
450
|
particle_matches_child?(child, leaf, repository,
|
|
433
451
|
parent: parent,
|
|
434
|
-
group_depth: group_depth
|
|
452
|
+
group_depth: group_depth,
|
|
453
|
+
owner_ns: owner_ns)
|
|
435
454
|
end
|
|
436
455
|
else
|
|
437
456
|
false
|
data/lib/lutaml/xsd/version.rb
CHANGED