lutaml-xsd 1.2.80 → 1.2.82
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 +58 -10
- data/lib/lutaml/xsd/validation/content_model_matcher.rb +22 -8
- data/lib/lutaml/xsd/validation/rules/attribute_validation_rule.rb +108 -11
- 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 +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c3da66790f16686077f84ff27947241c3fd9290e4a36f92d5b26ee70d340af4
|
|
4
|
+
data.tar.gz: 205af1e6a170523b054c34ba0cf43b86203877784bfe471ef88e47bcdbf60ae9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6901b1b6a2fc09e00325fe3e24df146097e6bf6967493e46e04d7eabe8b7a9183cdccc2cc8ccf39cfd9d0f3cf90157b0647c772f61512a68085c041fbf1329be
|
|
7
|
+
data.tar.gz: 221ce2f9ce160b0524a99cb9dfb029ccc10040adbe8282e8cb37ecbc94cc9f689e1a6b2e28a6ca27cd069ef2c6547a5d728f37a3254b5fc501b9cce50d27118c
|
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 | 4578/4696 (97.49%) |
|
|
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,8 @@ 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
|
|
146
|
+
round) → 4578 (97.49%, attribute-prohibition round), +617 net over the 1.2.43 baseline. The escapes round (PR #197 — \d→\p{Nd},
|
|
146
147
|
\w/\W/\s to their XSD value spaces) collapses Regex 120→69;
|
|
147
148
|
same-step pattern facets now behave as alternatives and
|
|
148
149
|
whitespace-only text reaches the facets intact (PR #200), Regex
|
|
@@ -232,15 +233,19 @@ corners.
|
|
|
232
233
|
|
|
233
234
|
| Class | Cases | Filed |
|
|
234
235
|
|---|---|---|
|
|
235
|
-
|
|
|
236
|
-
|
|
|
237
|
-
|
|
|
238
|
-
|
|
|
239
|
-
|
|
|
240
|
-
|
|
|
236
|
+
| Empty content against a fixed element value rejected (1.9.402.0 regression — isDefault001/002/076, ctZ013a, elemQ019, stE065/066/096, particlesZ040; attribute leg addB104, attO006) | 15 | leptris-rs#25 (+ case-list comment) |
|
|
237
|
+
| Content model enforced on NILLED elements (QFE1700g2, particlesIa002) | 2 | leptris-rs#26 |
|
|
238
|
+
| dateTime enumeration rejects timezone-equal forms (dateTime_enumeration005a/b) | 2 | leptris-rs#27 |
|
|
239
|
+
| "-0" rejected against nonNegativeInteger (errE002) | 1 | leptris-rs#28 |
|
|
240
|
+
| xsi:type override / substitution-member type binding false rejects (elemZ002, elemZ020) | 2 | leptris-rs#29 |
|
|
241
|
+
| Built-in lexical tables (char-reference shapes reZ003v/004v/005v/006v/006i, reK88; \D vintage reT51) | 7 | leptris/leptris#1936 (+ case-list comment) |
|
|
242
|
+
| Facade strict re-check on composed schemas — attributes (attgD005/019/020/021/022/025/027/029/031/033/034, attJ018) and composed type binding (schF1/G2/G7/P2) | 15 | moxml#510 (+ consolidated evidence comment) |
|
|
241
243
|
| Unresolvable include/import/redefine schemaLocation raises at parse instead of warning (anyURI_a001/a002, schD7, schG8) | 4 | lutaml-model#1087 |
|
|
242
|
-
|
|
|
243
|
-
|
|
|
244
|
+
| 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) |
|
|
245
|
+
| Redefine self-extension drops the extension content model | 0 — FIXED in leptris 1.9.396 | leptris#1926 (closed) |
|
|
246
|
+
| Ruby-side under-detections and false rejects (Regex dialect 18 — reJ/reS/reDH disputed families excluded; DataTypes corners duration028-030/dateTime013/gMonthDay006/dtZ86723; IC idK/idZ singletons; SimpleType stZ; Wildcards wildZ013; Group/Particles stragglers; Schema schT/D10/F2/errC007; Q032 chameleon import; particlesA013/A002/Ie004/Ig003/Ob012/015/Z001; mgO006/034; attF001/attJ002/003/attLa005; isDefault079) | ~55 | OURS — the remaining Ruby work queue |
|
|
247
|
+
|
|
248
|
+
Engine-attribution census (2026-11-03): of the 123 current failures, 50 carry engine findings (all attributed above), 73 are Ruby-side.
|
|
244
249
|
|
|
245
250
|
Suite history (same-stack where known): 71.8% baseline → 83.5%
|
|
246
251
|
(pre-NFA) → 83.6% (NFA + guard, 1.2.41) → 84.3% (#180) → 86.0%
|
|
@@ -422,6 +427,49 @@ tuple keys, node-set field violations and absent-field rows handled, cross-eleme
|
|
|
422
427
|
4569→4562 raw; 4563 with this round. NET -6 documented, not
|
|
423
428
|
chased — the fixed-empty leg reverses the moment leptris-rs#25
|
|
424
429
|
lands.
|
|
430
|
+
- GROUP-CONTENT ROUND (2026-11-02): +13 (Group 8→0 — the family is
|
|
431
|
+
CLEAN at 126/126; Particles Db001/Db006 +2, Ea004/011/018 +3; suite
|
|
432
|
+
4563→4573 = 97.38% on the drifted stack, rspec green). FOUR Ruby
|
|
433
|
+
defects: (a) resolve_group scanned only top-level schemas — a group
|
|
434
|
+
declared in an IMPORTED document (the extension-body shape)
|
|
435
|
+
resolved to nothing (groupB009/010); nested resolved schemas are
|
|
436
|
+
now searched depth-guarded. (b) The NFA wiring missed the
|
|
437
|
+
lutaml-model container classes: a group whose declaration is xs:all
|
|
438
|
+
compiled ORDERED (groupO004), and with_occurrence gave min=0
|
|
439
|
+
particles one phantom extra copy (min=0/max=1 compiled capacity 2 —
|
|
440
|
+
masked for bare elements by the occurrence rule, visible wherever
|
|
441
|
+
nested group bounds lean on this verdict: groupJ003/J006/J008/L008
|
|
442
|
+
accepted forbidden repeats); a max=0 particle compiles to the empty
|
|
443
|
+
fragment (groupH008). (c) type_particles had no complexContent
|
|
444
|
+
restriction branch — a restriction body compiled EMPTY and children
|
|
445
|
+
passed unchecked (groupH008); a restriction replaces the base
|
|
446
|
+
content model. (d) The ref owner-namespace probe resolved ANY
|
|
447
|
+
declaration — including inline ones — so a qualified local namesake
|
|
448
|
+
hijacked unprefixed refs; the probe is GLOBAL-only (resolve_global),
|
|
449
|
+
the unprefixed ref canonically names a no-namespace declaration,
|
|
450
|
+
several distinct prefixed forms of one local name mean deliberate
|
|
451
|
+
disambiguation (no owner guess), and an unresolvable ref matches by
|
|
452
|
+
local name alone. KNOWN GAP left open: particlesQ032 — a
|
|
453
|
+
no-namespace document imported under an EXPLICIT namespace: its
|
|
454
|
+
globals must adopt the import namespace (MS-lenient chameleon
|
|
455
|
+
import; our index keeps them no-namespace so the four e2 refs
|
|
456
|
+
mis-resolve). Zero regressions; the drift entries above remain
|
|
457
|
+
(fixed-empty 9, reU6).
|
|
458
|
+
- ATTRIBUTE-PROHIBITION ROUND (2026-11-03): +5 Attribute (9→4,
|
|
459
|
+
attF001/J002/J003/Z007i under-detections + attLa005 fixed-facet;
|
|
460
|
+
suite 4573→4578 = 97.49%, ZERO regressions, rspec green).
|
|
461
|
+
use=prohibited was never enforced — instances could carry a
|
|
462
|
+
prohibited attribute unchecked. The declaration binds now, with
|
|
463
|
+
THREE suite-arbitrated exemptions for incoherent shapes: a
|
|
464
|
+
wildcard admitting the attribute shadows it (attZ002); fixed +
|
|
465
|
+
prohibited is an illegal combo treated as inert (attP031); a
|
|
466
|
+
prohibited member inside an attributeGroup DEFINITION is illegal
|
|
467
|
+
placement the suite ignores (attZ015). A derived type's OWN
|
|
468
|
+
declaration overrides the base chain's prohibited (attZ007v) while
|
|
469
|
+
a restriction body's own prohibited stands (attZ007i). A ref's
|
|
470
|
+
use-site fixed overlays the global declaration in the value check
|
|
471
|
+
(attLa005). Attribute remainder (4): attJ018 + attO009/011
|
|
472
|
+
(engine-silent — queue), attO006 (leptris-rs#25 attribute leg).
|
|
425
473
|
- REGEX SEMANTICS ROUND (2026-10-31, PR #208): \w/\W now carry their
|
|
426
474
|
Appendix F complements (\w = [^\p{P}\p{Z}\p{C}], class interiors
|
|
427
475
|
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,11 +94,24 @@ module Lutaml
|
|
|
94
94
|
resolved_attributes,
|
|
95
95
|
xml_attributes, collector)
|
|
96
96
|
|
|
97
|
+
# Step 1b: Check prohibited attributes are absent — the
|
|
98
|
+
# declaration exists, its USE does not (attF001). Three
|
|
99
|
+
# suite-arbitrated exemptions: a wildcard that admits the
|
|
100
|
+
# attribute shadows the prohibition (attZ002); fixed plus
|
|
101
|
+
# prohibited is an illegal declaration combo and the suite
|
|
102
|
+
# treats the whole declaration as inert (attP031); and a
|
|
103
|
+
# prohibition arriving through DERIVATION is refinement
|
|
104
|
+
# incoherence the suite ignores (attZ007v, attZ015).
|
|
105
|
+
validate_prohibited_attributes(xml_element, schema_attributes,
|
|
106
|
+
xml_attributes, collector,
|
|
107
|
+
wildcards: wildcards)
|
|
108
|
+
|
|
97
109
|
# Step 2: Validate attribute values
|
|
98
110
|
validate_attribute_values(xml_element, resolved_attributes,
|
|
99
111
|
xml_attributes, collector,
|
|
100
112
|
repository: repository,
|
|
101
|
-
schema_element: schema_element
|
|
113
|
+
schema_element: schema_element,
|
|
114
|
+
raw_attrs: schema_attributes)
|
|
102
115
|
|
|
103
116
|
# Step 3: Check for unexpected attributes
|
|
104
117
|
validate_no_unexpected_attributes(xml_element,
|
|
@@ -224,9 +237,35 @@ module Lutaml
|
|
|
224
237
|
attributes.concat(inherited_attributes(part.base, repository)) if part
|
|
225
238
|
end
|
|
226
239
|
|
|
240
|
+
override_base_prohibitions(schema_def, attributes)
|
|
227
241
|
attributes
|
|
228
242
|
end
|
|
229
243
|
|
|
244
|
+
# A derived type's OWN (type-level or derivation-body)
|
|
245
|
+
# non-prohibited declaration for a name overrides the base
|
|
246
|
+
# chain's prohibited (attZ007v: the extension re-adds att1
|
|
247
|
+
# required over Two's prohibited). A restriction body's own
|
|
248
|
+
# prohibited stands against the inherited plain declaration
|
|
249
|
+
# (attZ007i: e2 carrying att1 is invalid).
|
|
250
|
+
#
|
|
251
|
+
# @return [void]
|
|
252
|
+
def override_base_prohibitions(schema_def, attributes)
|
|
253
|
+
own_declarations = Array(schema_def.attribute)
|
|
254
|
+
content = schema_def.complex_content || schema_def.simple_content
|
|
255
|
+
CONTENT_PART_KINDS.each do |kind|
|
|
256
|
+
part = content && Array(content.public_send(kind)).first
|
|
257
|
+
own_declarations += Array(part&.attribute) if part
|
|
258
|
+
end
|
|
259
|
+
own_names = own_declarations
|
|
260
|
+
.reject { |a| a.use.to_s == "prohibited" }
|
|
261
|
+
.map { |a| a.name.to_s }
|
|
262
|
+
attributes.reject! do |a|
|
|
263
|
+
a.use.to_s == "prohibited" &&
|
|
264
|
+
own_names.include?(a.name.to_s) &&
|
|
265
|
+
!own_declarations.include?(a)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
230
269
|
# The base type's attribute set flows into the derived type
|
|
231
270
|
# through extension and restriction alike.
|
|
232
271
|
#
|
|
@@ -275,7 +314,11 @@ module Lutaml
|
|
|
275
314
|
return [] unless group
|
|
276
315
|
|
|
277
316
|
own = (group.name || group.ref).to_s.split(":").last
|
|
278
|
-
|
|
317
|
+
# use=prohibited is illegal inside an attributeGroup
|
|
318
|
+
# DEFINITION (attZ015): the suite ignores such members.
|
|
319
|
+
attrs = Array(group.attribute).reject do |attr|
|
|
320
|
+
attr.use.to_s == "prohibited"
|
|
321
|
+
end
|
|
279
322
|
|
|
280
323
|
# An attributeGroup may reference other attributeGroups —
|
|
281
324
|
# their declarations flow into the expansion (attgD004). A
|
|
@@ -393,6 +436,44 @@ module Lutaml
|
|
|
393
436
|
schema_attr.use == "required"
|
|
394
437
|
end
|
|
395
438
|
|
|
439
|
+
# A prohibited attribute is DECLARED but its use is banned:
|
|
440
|
+
# any instance occurrence violates the schema (attF001).
|
|
441
|
+
#
|
|
442
|
+
# @return [void]
|
|
443
|
+
def validate_prohibited_attributes(xml_element, schema_attrs,
|
|
444
|
+
xml_attrs, collector,
|
|
445
|
+
wildcards: [])
|
|
446
|
+
schema_attrs.each do |schema_attr|
|
|
447
|
+
next unless schema_attr.use.to_s == "prohibited"
|
|
448
|
+
# fixed + prohibited is an illegal declaration combo
|
|
449
|
+
# (attP031): the suite treats the declaration as inert.
|
|
450
|
+
next if schema_attr.fixed
|
|
451
|
+
|
|
452
|
+
attr_name = schema_attr.name.to_s
|
|
453
|
+
next if attr_name.empty?
|
|
454
|
+
|
|
455
|
+
xml_attr = xml_attrs.find { |xa| xa.name == attr_name }
|
|
456
|
+
next unless xml_attr
|
|
457
|
+
next if wildcards.any? do |wildcard|
|
|
458
|
+
wildcard_permits_attribute?(wildcard, xml_element, xml_attr,
|
|
459
|
+
nil)
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
report_error(
|
|
463
|
+
collector,
|
|
464
|
+
code: "prohibited_attribute_present",
|
|
465
|
+
message: "Attribute '#{attr_name}' is prohibited here " \
|
|
466
|
+
"but present",
|
|
467
|
+
location: xml_element.xpath,
|
|
468
|
+
context: {
|
|
469
|
+
attribute: attr_name,
|
|
470
|
+
element: xml_element.name,
|
|
471
|
+
},
|
|
472
|
+
suggestion: "Remove attribute: #{attr_name}",
|
|
473
|
+
)
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
396
477
|
# Validate attribute values
|
|
397
478
|
#
|
|
398
479
|
# @param xml_element [XmlElement] The element
|
|
@@ -402,14 +483,23 @@ module Lutaml
|
|
|
402
483
|
# @return [void]
|
|
403
484
|
def validate_attribute_values(xml_element, schema_attrs, xml_attrs,
|
|
404
485
|
collector, repository: nil,
|
|
405
|
-
schema_element: nil)
|
|
486
|
+
schema_element: nil, raw_attrs: [])
|
|
406
487
|
xml_attrs.each do |xml_attr|
|
|
407
488
|
schema_attr = find_schema_attribute(schema_attrs, xml_attr,
|
|
408
489
|
repository, schema_element)
|
|
409
490
|
next unless schema_attr
|
|
410
491
|
|
|
492
|
+
# A ref's use site may carry facets the global declaration
|
|
493
|
+
# lacks (attLa005: ref="x:ga1" fixed="abc" at the use
|
|
494
|
+
# site); the instance value is checked against them too.
|
|
495
|
+
use_site = raw_attrs.find do |r|
|
|
496
|
+
r.name == schema_attr.name ||
|
|
497
|
+
r.ref.to_s.split(":").last == schema_attr.name
|
|
498
|
+
end
|
|
411
499
|
validate_attribute_value(xml_element, xml_attr, schema_attr,
|
|
412
|
-
collector,
|
|
500
|
+
collector,
|
|
501
|
+
repository: repository,
|
|
502
|
+
use_site: use_site)
|
|
413
503
|
end
|
|
414
504
|
end
|
|
415
505
|
|
|
@@ -461,10 +551,11 @@ module Lutaml
|
|
|
461
551
|
# "123").
|
|
462
552
|
#
|
|
463
553
|
# @return [Boolean]
|
|
464
|
-
def fixed_value_matches?(value, schema_attr)
|
|
554
|
+
def fixed_value_matches?(value, schema_attr, fixed_override = nil)
|
|
555
|
+
fixed = fixed_override || schema_attr.fixed
|
|
465
556
|
rule = attribute_whitespace_rule(schema_attr)
|
|
466
557
|
canonical_lexical(value, rule) ==
|
|
467
|
-
canonical_lexical(
|
|
558
|
+
canonical_lexical(fixed, rule)
|
|
468
559
|
end
|
|
469
560
|
|
|
470
561
|
# @return [Symbol] :preserve, :replace or :collapse
|
|
@@ -499,21 +590,27 @@ module Lutaml
|
|
|
499
590
|
# @param collector [ResultCollector] Result collector
|
|
500
591
|
# @return [void]
|
|
501
592
|
def validate_attribute_value(xml_element, xml_attr, schema_attr,
|
|
502
|
-
collector, repository: nil
|
|
593
|
+
collector, repository: nil,
|
|
594
|
+
use_site: nil)
|
|
503
595
|
value = xml_attr.value
|
|
596
|
+
fixed = if use_site && !use_site.fixed.nil?
|
|
597
|
+
use_site.fixed
|
|
598
|
+
else
|
|
599
|
+
schema_attr.fixed
|
|
600
|
+
end
|
|
504
601
|
|
|
505
602
|
# Check fixed value constraint
|
|
506
|
-
if
|
|
507
|
-
unless fixed_value_matches?(value, schema_attr)
|
|
603
|
+
if fixed
|
|
604
|
+
unless fixed_value_matches?(value, schema_attr, fixed)
|
|
508
605
|
report_error(
|
|
509
606
|
collector,
|
|
510
607
|
code: "fixed_attribute_value_mismatch",
|
|
511
608
|
message: "Attribute '#{xml_attr.name}' must have fixed " \
|
|
512
|
-
"value '#{
|
|
609
|
+
"value '#{fixed}'",
|
|
513
610
|
location: xml_element.xpath,
|
|
514
611
|
context: {
|
|
515
612
|
attribute: xml_attr.name,
|
|
516
|
-
expected:
|
|
613
|
+
expected: fixed,
|
|
517
614
|
actual: value,
|
|
518
615
|
},
|
|
519
616
|
)
|
|
@@ -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
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-xsd
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.82
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: liquid
|