lutaml-xsd 1.2.80 → 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 +30 -2
- data/lib/lutaml/xsd/validation/content_model_matcher.rb +22 -8
- data/lib/lutaml/xsd/validation/rules/content_model_validation_rule.rb +48 -12
- data/lib/lutaml/xsd/validation/schema_definition_resolver.rb +44 -1
- data/lib/lutaml/xsd/validation/validation_rule.rb +8 -3
- 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,7 +120,7 @@ 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
125
|
(2026-11-02, bundled-stack measurement with the escape,
|
|
126
126
|
hint-following, pattern-alternative, identity-crash, engine-set,
|
|
@@ -142,7 +142,7 @@ lockfile refresh) → 4411 (93.9%) → 4430 (94.3%) → 4435 (94.4%) →
|
|
|
142
142
|
4436 (94.5%) → 4439 (94.5%) → 4454 (94.9%) → 4495 (95.7%) → 4524
|
|
143
143
|
(96.3%) → 4534 (96.6%) → 4540 (96.7%) → 4542 (96.7%) → 4546 (96.8%)
|
|
144
144
|
→ 4557 (97.0%) → 4565 (97.2%) → 4569 (97.3%) → 4562 (drift raw) →
|
|
145
|
-
4563 (97.17%, ref-namespace round), +
|
|
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},
|
|
146
146
|
\w/\W/\s to their XSD value spaces) collapses Regex 120→69;
|
|
147
147
|
same-step pattern facets now behave as alternatives and
|
|
148
148
|
whitespace-only text reaches the facets intact (PR #200), Regex
|
|
@@ -422,6 +422,34 @@ tuple keys, node-set field violations and absent-field rows handled, cross-eleme
|
|
|
422
422
|
4569→4562 raw; 4563 with this round. NET -6 documented, not
|
|
423
423
|
chased — the fixed-empty leg reverses the moment leptris-rs#25
|
|
424
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).
|
|
425
453
|
- REGEX SEMANTICS ROUND (2026-10-31, PR #208): \w/\W now carry their
|
|
426
454
|
Appendix F complements (\w = [^\p{P}\p{Z}\p{C}], class interiors
|
|
427
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
|
|
@@ -299,6 +299,7 @@ module Lutaml
|
|
|
299
299
|
type.complex_content
|
|
300
300
|
content = type.complex_content
|
|
301
301
|
extension = Array(content.extension).first
|
|
302
|
+
restriction = Array(content.restriction).first
|
|
302
303
|
if extension
|
|
303
304
|
base_local = extension.base.to_s.split(":").last
|
|
304
305
|
base_type = base_local && repository&.resolve_named_type(base_local)
|
|
@@ -311,6 +312,14 @@ module Lutaml
|
|
|
311
312
|
particles << particle
|
|
312
313
|
end
|
|
313
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
|
|
314
323
|
end
|
|
315
324
|
end
|
|
316
325
|
|
|
@@ -508,12 +517,16 @@ module Lutaml
|
|
|
508
517
|
|
|
509
518
|
# A qualified ref names a declaration in ONE namespace.
|
|
510
519
|
# Global names duplicated across namespaces (import + main)
|
|
511
|
-
# resolve against the child's namespace today;
|
|
512
|
-
#
|
|
513
|
-
#
|
|
514
|
-
#
|
|
515
|
-
#
|
|
516
|
-
#
|
|
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.
|
|
517
530
|
#
|
|
518
531
|
# @return [void]
|
|
519
532
|
def scan_cross_namespace_refs(xml_element, particles, collector,
|
|
@@ -526,12 +539,35 @@ module Lutaml
|
|
|
526
539
|
end
|
|
527
540
|
return if refs.empty?
|
|
528
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
|
+
|
|
529
552
|
xml_element.children.each do |child|
|
|
530
|
-
|
|
531
|
-
next
|
|
532
|
-
|
|
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
|
|
533
569
|
end
|
|
534
|
-
next unless
|
|
570
|
+
next unless hit
|
|
535
571
|
next if wildcard_covers_child?(xml_element, particles, child,
|
|
536
572
|
repository)
|
|
537
573
|
|
|
@@ -540,7 +576,7 @@ module Lutaml
|
|
|
540
576
|
code: "unexpected_element_in_sequence",
|
|
541
577
|
message: "Unexpected element '#{child.qualified_name}' " \
|
|
542
578
|
"in sequence — the reference names the same " \
|
|
543
|
-
"declaration in
|
|
579
|
+
"declaration in another namespace",
|
|
544
580
|
location: child.xpath,
|
|
545
581
|
context: { element: child.qualified_name },
|
|
546
582
|
suggestion: "Use the declaration's namespace or rename " \
|
|
@@ -563,7 +599,7 @@ module Lutaml
|
|
|
563
599
|
return nil unless repository
|
|
564
600
|
|
|
565
601
|
local = particle.ref.to_s.split(":").last
|
|
566
|
-
(owner_ns && repository.
|
|
602
|
+
(owner_ns && repository.resolve_global(local, owner_ns)) ||
|
|
567
603
|
repository.resolve(local, child.namespace_uri) ||
|
|
568
604
|
repository.resolve(local, nil)
|
|
569
605
|
end
|
|
@@ -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
|
|
@@ -252,12 +252,17 @@ module Lutaml
|
|
|
252
252
|
# owner_ns nil means UNKNOWN (the caller has no owner
|
|
253
253
|
# context), not "no namespace" — resolve(local, nil) only
|
|
254
254
|
# matches no-namespace declarations, and a hint-loaded
|
|
255
|
-
# document can supply a foreign namesake.
|
|
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.
|
|
256
258
|
declaration =
|
|
257
|
-
(owner_ns && repository&.
|
|
259
|
+
(owner_ns && repository&.resolve_global(local, owner_ns)) ||
|
|
258
260
|
repository&.resolve(local, child.namespace_uri) ||
|
|
259
261
|
repository&.resolve(local, nil)
|
|
260
|
-
|
|
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
|
|
261
266
|
|
|
262
267
|
child.name == declaration.name &&
|
|
263
268
|
namespaces_match?(child.namespace_uri,
|
data/lib/lutaml/xsd/version.rb
CHANGED