lutaml-xsd 1.2.112 → 1.2.114
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 +74 -2
- data/lib/lutaml/xsd/validation/content_model_matcher.rb +6 -3
- data/lib/lutaml/xsd/validation/occurrence_product_guard.rb +6 -2
- data/lib/lutaml/xsd/validation/rule_engine.rb +30 -11
- data/lib/lutaml/xsd/validation/rules/content_model_validation_rule.rb +1 -0
- data/lib/lutaml/xsd/validation/rules/occurrence_validation_rule.rb +9 -1
- data/lib/lutaml/xsd/validation/rules/type_validation_rule.rb +226 -23
- data/lib/lutaml/xsd/validation/schema_definition_resolver.rb +19 -8
- data/lib/lutaml/xsd/validation/value_space.rb +73 -0
- 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: 36f7c7c0e9d599f7400dac905f372692d9da5d253bd5d67069106d923259a4b3
|
|
4
|
+
data.tar.gz: 6a2b5a3b8f8db1067381b3e8f924da0f06af636a152626795167b97012510e40
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c9467308b09e8249e3e5b87a1b1a2d298f95d55ec4bf466a63a0a376752e813f0f9753b821d25c000e3d39a0c65249e675df39ec1496f47ecadde12b5782d28d
|
|
7
|
+
data.tar.gz: f147cc2f3ebb9d6ec7ca62601b72573ccc70433bdd36b50cbe46133e815b052ac5a4fec48153e3e2823ab8c6d1f3955f13f1f1714b761b4bfc1310de174b5c20
|
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 | 4658/4696 (99.19%) — first measure above 99 |
|
|
124
124
|
|
|
125
125
|
(2026-11-02, bundled-stack measurement with the escape,
|
|
126
126
|
hint-following, pattern-alternative, identity-crash, engine-set,
|
|
@@ -144,7 +144,7 @@ lockfile refresh) → 4411 (93.9%) → 4430 (94.3%) → 4435 (94.4%) →
|
|
|
144
144
|
→ 4557 (97.0%) → 4565 (97.2%) → 4569 (97.3%) → 4562 (drift raw) →
|
|
145
145
|
4563 (97.17%, ref-namespace round) → 4573 (97.38%, group-content
|
|
146
146
|
round) → 4578 (97.49%, attribute-prohibition round) → 4585 (97.64%,
|
|
147
|
-
multi-document round) → 4588 (97.7%, type-binding round) → 4592 (97.79%, model-161 ride) → 4611 (98.19%, engine-406 ride) → 4613 (98.23%, temporal-enumeration round) → 4614 (98.25%, simple-type-children round) → 4617 (98.32%, ic-ancestor-scope round) → 4637 (parent-context rounds + chameleon round 1.2.109) → 4637 (nested-type-candidates round, 1.2.110) → 4637 (redefine-cycle round, 1.2.111) → 4644 measured (98.96% of measured, train-adoption round: moxml 0.6.6 + leptris 1.9.418.0), +
|
|
147
|
+
multi-document round) → 4588 (97.7%, type-binding round) → 4592 (97.79%, model-161 ride) → 4611 (98.19%, engine-406 ride) → 4613 (98.23%, temporal-enumeration round) → 4614 (98.25%, simple-type-children round) → 4617 (98.32%, ic-ancestor-scope round) → 4637 (parent-context rounds + chameleon round 1.2.109) → 4637 (nested-type-candidates round, 1.2.110) → 4637 (redefine-cycle round, 1.2.111) → 4644 measured (98.96% of measured, train-adoption round: moxml 0.6.6 + leptris 1.9.418.0) → 4649 (98.91%, quadratic-dispatch round — first FULL-denominator measure, the 5 reZ budget exclusions retired) → 4658 (99.19%, remainder-classification round — first measure above 99), +697 net over the 1.2.43 baseline. The escapes round (PR #197 — \d→\p{Nd},
|
|
148
148
|
\w/\W/\s to their XSD value spaces) collapses Regex 120→69;
|
|
149
149
|
same-step pattern facets now behave as alternatives and
|
|
150
150
|
whitespace-only text reaches the facets intact (PR #200), Regex
|
|
@@ -612,6 +612,78 @@ tuple keys, node-set field violations and absent-field rows handled, cross-eleme
|
|
|
612
612
|
the RAW validity boolean — expected-INVALID rows reading false
|
|
613
613
|
are PASSES; the first assembly inverted them and manufactured 9
|
|
614
614
|
phantom reZ regressions before the correction.
|
|
615
|
+
- QUADRATIC-DISPATCH ROUND (2026-11-12, release pending): the
|
|
616
|
+
per-rule profile on reZ003v (a trivial `[\w]` pattern over a
|
|
617
|
+
232KB instance of ~65k `<value>` siblings) showed the RULES at
|
|
618
|
+
~3.3s and the DISPATCH at ~100-370s — `same_name_sibling_index`
|
|
619
|
+
scanned the parent's raw children per element (O(N) per child,
|
|
620
|
+
O(N²) per parent) TIMES five category walks, and
|
|
621
|
+
`positional_children` re-walked the parent's particle tree per
|
|
622
|
+
element. THREE cuts, all semantics-preserving: (a) the sibling
|
|
623
|
+
ordinal is now a per-parent identity-keyed map built in ONE
|
|
624
|
+
O(N) pass (running same-name count — identical to the old
|
|
625
|
+
take(position).count reading); (b) the dispatch skips the scan
|
|
626
|
+
when the content model holds at most ONE same-named candidate
|
|
627
|
+
(positional binding only matters for DUPLICATE declarations,
|
|
628
|
+
isDefault079); (c) positional_children memoizes per
|
|
629
|
+
(declaration, name) on the resolver (schema immutable once
|
|
630
|
+
indexed). reZ003v validate 373.6s → 2.94s (127×); rspec 1663
|
|
631
|
+
green and 40s faster; rubocop clean. LEDGER: 4649/4696
|
|
632
|
+
(98.91%) — the FIRST FULL-DENOMINATOR MEASURE of the campaign:
|
|
633
|
+
ALL 94 batches completed in one rake run, the 5 reZ budget
|
|
634
|
+
exclusions RETIRED (reZ003v/005v/005i/006v/006i measured and
|
|
635
|
+
PASSING in-run), and the batch-03000 carry-over convention
|
|
636
|
+
RETIRED (reI003's tax was the same dispatch overhead, not regex
|
|
637
|
+
backtracking — 50/50 fresh). ZERO regressions vs the 1.2.112
|
|
638
|
+
ledger (47 still-failing in both — the standing remainder:
|
|
639
|
+
contested 24, the upstream filings' legs, the parked redesign
|
|
640
|
+
tails).
|
|
641
|
+
- REMAINDER-CLASSIFICATION ROUND (2026-11-12, release pending):
|
|
642
|
+
the live ledger re-derived the queue (the memory's schA1/D10
|
|
643
|
+
gaps were STALE — all three pass since the chameleon/type-
|
|
644
|
+
binding rounds); the 47-case remainder splits into 13
|
|
645
|
+
filed-upstream, ~25 contested/disputed, and NINE ours. ALL
|
|
646
|
+
NINE FIXED, zero regressions — four mechanism families:
|
|
647
|
+
(a) DERIVATION COMPLETENESS (stZ054/055/056/073b/ba): §4.1.2
|
|
648
|
+
anySimpleType is the ur-simple root — EVERY simple type derives
|
|
649
|
+
from it, restriction-of-builtin chains and pure lists/unions
|
|
650
|
+
alike, and a simpleContent derivation over a builtin reaches it
|
|
651
|
+
transitively; a BUILTIN override owes the §3.3.4 check against
|
|
652
|
+
NAMED declared types via the BUILTIN_ANCESTORS table
|
|
653
|
+
(ValueSpace) — identity or declared-in-ancestry, xs:anyType
|
|
654
|
+
derives from nothing but itself (stZ056), restriction-of-union
|
|
655
|
+
does NOT admit members (stZ073b), direct unions do (the
|
|
656
|
+
elemT071 rule, builtin members included). (b) NFA LOOP-BRANCH
|
|
657
|
+
BYPASS (particlesA013 + the matcher probe): with_occurrence's
|
|
658
|
+
unbounded branch wired ε-edges from the FIRST body copy's exit
|
|
659
|
+
to final — with min ≥ 2 those bypass the mandatory unrolled
|
|
660
|
+
copies and accept after one consumption; the edges are
|
|
661
|
+
redundant at min ≤ 1 (cursor == exit_state) and removed.
|
|
662
|
+
(c) CHOICE-MIN EMPTY BRANCH (particlesIe004): a branch whose
|
|
663
|
+
minimum is zero satisfies the choice with the EMPTY sequence —
|
|
664
|
+
the choice's own minimum cannot fail on a zero count.
|
|
665
|
+
(d) BLOCK IN WHOLE OR IN PART + EXTENSION TEXT (particlesIg003,
|
|
666
|
+
addB114): §3.3.4 blocks the derivation CHAIN — any_type_chain_
|
|
667
|
+
kinds walks every hop to the ur-type (a bare complexType
|
|
668
|
+
restricts anyType per §3.4.3, so ext→base→anyType carries a
|
|
669
|
+
restriction hop that block="restriction" catches), and a
|
|
670
|
+
simpleContent EXTENSION's text validates against the
|
|
671
|
+
extension's base type (addB114: '62' violates base A's facets
|
|
672
|
+
through E-A; the branch previously had only a comment).
|
|
673
|
+
Also routed products AT the OccurrenceProductGuard cap to the
|
|
674
|
+
Ruby NFA (`< CAP` — the engine answers lax on wildcard
|
|
675
|
+
minimums there) and admitted the parsed Xml::Schema::Xsd::Any
|
|
676
|
+
class to PARTICLE_TYPES + the matcher wildcard dispatch.
|
|
677
|
+
SUITE 4658/4696 (99.19%) — first measure above 99; rspec 1663
|
|
678
|
+
green; rubocop clean. REMAINDER 38: 13 filed-upstream
|
|
679
|
+
(leptris-rs#72/#73, #45 legs, attgC035-038), ~25
|
|
680
|
+
contested/disputed (reJ 14, reS/reT/reU/reDH vintages,
|
|
681
|
+
attZ014a/b 1.1 drift, gMonth_pattern/double018, particlesZ001).
|
|
682
|
+
TOOLING NOTE: rubocop -A broke the tri-state
|
|
683
|
+
builtin_derivation_cover — the nil ("not a builtin") became
|
|
684
|
+
false, which would have flagged every schema-type override
|
|
685
|
+
undervalued; caught by re-reading the -A diff (the standing
|
|
686
|
+
predicate-nil lesson, now with the rename fix applied).
|
|
615
687
|
- TRAIN ADOPTION REGRESSION — CHARACTER REFERENCES (2026-11-11):
|
|
616
688
|
the 2026-11-10 adoption verdict ("conformance-IDENTICAL") was
|
|
617
689
|
WRONG for one class: moxml 0.6.4 stopped decoding numeric
|
|
@@ -195,7 +195,7 @@ module Lutaml
|
|
|
195
195
|
with_occurrence(particle) { compile_all(particle) }
|
|
196
196
|
when Lutaml::Xml::Schema::Xsd::Group
|
|
197
197
|
compile_group(particle)
|
|
198
|
-
when Lutaml::Xsd::Any
|
|
198
|
+
when Lutaml::Xsd::Any, Lutaml::Xml::Schema::Xsd::Any
|
|
199
199
|
with_occurrence(particle) { leaf_fragment(:wildcard, particle) }
|
|
200
200
|
else
|
|
201
201
|
with_occurrence(particle) { leaf_fragment(:element, particle) }
|
|
@@ -330,10 +330,13 @@ module Lutaml
|
|
|
330
330
|
@epsilons[entry] << final if min.zero?
|
|
331
331
|
if max == UNBOUNDED || min > MAX_UNROLL ||
|
|
332
332
|
(max != UNBOUNDED && max - min > MAX_UNROLL)
|
|
333
|
+
# Loop-or-finish from the LAST mandatory copy's exit only.
|
|
334
|
+
# The first copy's exit must NOT shortcut to final — with
|
|
335
|
+
# min >= 2 the ε-edge would bypass the mandatory copies and
|
|
336
|
+
# accept after one consumption (particlesA013: any min=2
|
|
337
|
+
# matched by a single child).
|
|
333
338
|
@epsilons[cursor] << last_entry
|
|
334
339
|
@epsilons[cursor] << final
|
|
335
|
-
@epsilons[exit_state] << last_entry
|
|
336
|
-
@epsilons[exit_state] << final
|
|
337
340
|
else
|
|
338
341
|
@epsilons[cursor] << final
|
|
339
342
|
optional = max - min - (min.zero? && max.is_a?(Integer) ? 1 : 0)
|
|
@@ -62,7 +62,11 @@ module Lutaml
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def particles_safe?(particles)
|
|
65
|
-
|
|
65
|
+
# Products AT the cap route to the Ruby NFA too — the engine
|
|
66
|
+
# compiles them into exactly the automata this guard exists
|
|
67
|
+
# to avoid (particlesA013: any min=2 max=1000000; the engine
|
|
68
|
+
# answers lax on the wildcard minimum there).
|
|
69
|
+
particles.all? { |particle| product(particle, 1, 0) < CAP }
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
# Walks a type's content model. Nested elements referencing
|
|
@@ -80,7 +84,7 @@ module Lutaml
|
|
|
80
84
|
expanded = @group_expander&.call(particle)
|
|
81
85
|
expanded && particles_safe?(expanded)
|
|
82
86
|
else
|
|
83
|
-
product(particle, 1, 0)
|
|
87
|
+
product(particle, 1, 0) < CAP
|
|
84
88
|
end
|
|
85
89
|
end
|
|
86
90
|
end
|
|
@@ -80,8 +80,17 @@ module Lutaml
|
|
|
80
80
|
resolver.resolve(parent.name, parent.namespace_uri)
|
|
81
81
|
candidates = resolver.positional_children(parent_declaration,
|
|
82
82
|
xml_element.name)
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
# The scan only matters when the parent's content model
|
|
84
|
+
# holds DUPLICATE same-named declarations (isDefault079);
|
|
85
|
+
# a sole candidate binds regardless of position — skipping
|
|
86
|
+
# the sibling scan keeps wide documents linear.
|
|
87
|
+
positional = if candidates.size > 1
|
|
88
|
+
candidates[same_name_sibling_index(
|
|
89
|
+
xml_element, parent
|
|
90
|
+
)]
|
|
91
|
+
else
|
|
92
|
+
candidates.first
|
|
93
|
+
end
|
|
85
94
|
# The binding must agree with the child's OWN namespace:
|
|
86
95
|
# an unqualified ref's global namesake never serves a
|
|
87
96
|
# qualified local namesake (particlesQ016).
|
|
@@ -245,21 +254,31 @@ module Lutaml
|
|
|
245
254
|
override && override.value.to_s.split(":").last
|
|
246
255
|
end
|
|
247
256
|
|
|
248
|
-
# The child's
|
|
257
|
+
# The child's ordinal among its same-named siblings — the index
|
|
249
258
|
# the positional binding (duplicate same-named declarations,
|
|
250
|
-
# isDefault079) binds to.
|
|
259
|
+
# isDefault079) binds to. Keyed on the RAW moxml nodes: the
|
|
251
260
|
# wrapper objects are not identity-stable across accessor calls
|
|
252
|
-
# (the IdUniquenessRule lesson).
|
|
261
|
+
# (the IdUniquenessRule lesson). The per-parent ordinal map is
|
|
262
|
+
# memoized — a document of N same-named siblings costs one
|
|
263
|
+
# O(N) pass per parent instead of an O(N) scan per child
|
|
264
|
+
# (reZ003v: 65k siblings made every dispatch quadratic, times
|
|
265
|
+
# five category walks).
|
|
253
266
|
#
|
|
254
267
|
# @return [Integer]
|
|
255
268
|
def same_name_sibling_index(element, parent)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
269
|
+
parent_raw = parent.moxml_element
|
|
270
|
+
cache = (@sibling_ordinals ||= {}.compare_by_identity)
|
|
271
|
+
ordinals = cache[parent_raw]
|
|
272
|
+
if ordinals.nil?
|
|
273
|
+
ordinals = {}.compare_by_identity
|
|
274
|
+
counts = Hash.new(0)
|
|
275
|
+
parent_raw.elements.each do |node|
|
|
276
|
+
ordinals[node] = counts[node.name]
|
|
277
|
+
counts[node.name] += 1
|
|
278
|
+
end
|
|
279
|
+
cache[parent_raw] = ordinals
|
|
259
280
|
end
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
raw_children.take(position).count { |node| node.name == element.name }
|
|
281
|
+
ordinals.fetch(element.moxml_element, 0)
|
|
263
282
|
end
|
|
264
283
|
|
|
265
284
|
# Whether the parent's content model holds a processContents
|
|
@@ -415,7 +415,15 @@ module Lutaml
|
|
|
415
415
|
repository)
|
|
416
416
|
occurrence_count = choice_occurrence_count(present_count, choice, repository)
|
|
417
417
|
|
|
418
|
-
|
|
418
|
+
# A branch whose occurrence minimum is zero satisfies the
|
|
419
|
+
# choice with the EMPTY sequence (XSD particle semantics),
|
|
420
|
+
# so the choice's own minimum cannot fail on a zero count
|
|
421
|
+
# (particlesIe004: choice(e1 1, e2 0) against an empty doc).
|
|
422
|
+
empty_branch = choice_alternatives(choice).any? do |alt|
|
|
423
|
+
parse_occurs(alt.min_occurs, 1).zero?
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
if occurrence_count < min_occurs && !empty_branch
|
|
419
427
|
report_error(
|
|
420
428
|
collector,
|
|
421
429
|
code: "choice_min_occurs_violation",
|
|
@@ -26,6 +26,14 @@ module Lutaml
|
|
|
26
26
|
include EngineBacked
|
|
27
27
|
include ValueSpace
|
|
28
28
|
|
|
29
|
+
# The ur-simple root: every simple type derives from it
|
|
30
|
+
# (XSD 1.0 §4.1.2).
|
|
31
|
+
ANY_SIMPLE_TYPE_LOCAL = "anySimpleType"
|
|
32
|
+
|
|
33
|
+
# The ur-type: root of the type hierarchy, derives from
|
|
34
|
+
# nothing but itself.
|
|
35
|
+
ANY_TYPE_LOCAL = "anyType"
|
|
36
|
+
|
|
29
37
|
# Rule category
|
|
30
38
|
#
|
|
31
39
|
# @return [Symbol] :type
|
|
@@ -62,14 +70,14 @@ module Lutaml
|
|
|
62
70
|
repository)
|
|
63
71
|
unless type_def
|
|
64
72
|
# xsi:type may name a built-in — resolve_named_type only
|
|
65
|
-
# sees schema documents (addB028).
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
# sees schema documents (addB028). A builtin override
|
|
74
|
+
# still owes the §3.3.4 derivation against the declared
|
|
75
|
+
# type: anyType derives from nothing else, so overriding
|
|
76
|
+
# an anySimpleType declaration with xs:anyType is
|
|
77
|
+
# invalid (stZ056).
|
|
78
|
+
return if builtin_override_reference(xml_element) &&
|
|
79
|
+
builtin_override_handled?(xml_element, schema_element,
|
|
80
|
+
collector, repository)
|
|
73
81
|
|
|
74
82
|
report_error(
|
|
75
83
|
collector,
|
|
@@ -136,7 +144,9 @@ module Lutaml
|
|
|
136
144
|
# declaration's own fixed content (isDefault077).
|
|
137
145
|
validate_fixed_content(xml_element, schema_element,
|
|
138
146
|
xml_element.text_content, collector)
|
|
139
|
-
validate_complex_type_content(xml_element, type_def, collector
|
|
147
|
+
validate_complex_type_content(xml_element, type_def, collector,
|
|
148
|
+
repository: repository,
|
|
149
|
+
declared: schema_element)
|
|
140
150
|
else
|
|
141
151
|
# Handle built-in types
|
|
142
152
|
validate_builtin_type(xml_element, type_def, collector,
|
|
@@ -172,7 +182,12 @@ module Lutaml
|
|
|
172
182
|
# from one (elemT071); the override's block-kind comes from
|
|
173
183
|
# the member chain (elemT074). Complex types derive through
|
|
174
184
|
# both content kinds — a simpleContent extension of a simple
|
|
175
|
-
# base is a derivation too (elemT058).
|
|
185
|
+
# base is a derivation too (elemT058). anySimpleType is the
|
|
186
|
+
# ur-simple root (§4.1.2): EVERY simple type derives from it
|
|
187
|
+
# — restriction-of-builtin chains and pure lists/unions alike
|
|
188
|
+
# (stZ054) — and a simpleContent derivation over a builtin
|
|
189
|
+
# base reaches it transitively (stZ055's extension of
|
|
190
|
+
# xs:string).
|
|
176
191
|
#
|
|
177
192
|
# @return [Boolean]
|
|
178
193
|
def validly_derived?(type, base_local, repository, depth = 0)
|
|
@@ -190,14 +205,19 @@ module Lutaml
|
|
|
190
205
|
part = derivation_part(type)
|
|
191
206
|
return false unless part
|
|
192
207
|
|
|
193
|
-
|
|
208
|
+
raw_base = part.base.to_s
|
|
209
|
+
next_local = raw_base.split(":").last
|
|
194
210
|
return true if next_local == base_local
|
|
211
|
+
return true if base_local == ANY_SIMPLE_TYPE_LOCAL &&
|
|
212
|
+
builtin_base?(raw_base)
|
|
195
213
|
|
|
196
214
|
validly_derived?(
|
|
197
215
|
repository&.resolve_named_type(next_local),
|
|
198
216
|
base_local, repository, depth + 1
|
|
199
217
|
)
|
|
200
218
|
when Lutaml::Xml::Schema::Xsd::SimpleType
|
|
219
|
+
return true if base_local == ANY_SIMPLE_TYPE_LOCAL
|
|
220
|
+
|
|
201
221
|
restriction = type.restriction
|
|
202
222
|
return false unless restriction
|
|
203
223
|
|
|
@@ -231,6 +251,18 @@ module Lutaml
|
|
|
231
251
|
end
|
|
232
252
|
end
|
|
233
253
|
|
|
254
|
+
# Whether a derivation base attribute names a BUILTIN type —
|
|
255
|
+
# the xs:/xsd:-prefixed spellings only. An unresolvable
|
|
256
|
+
# UNPREFIXED base is a lenient-fallback miss, never a builtin
|
|
257
|
+
# (the string-fallback lesson).
|
|
258
|
+
#
|
|
259
|
+
# @param raw_base [String]
|
|
260
|
+
# @return [Boolean]
|
|
261
|
+
def builtin_base?(raw_base)
|
|
262
|
+
prefix = raw_base.to_s.split(":", 2).first
|
|
263
|
+
ValueSpace::BUILTIN_PREFIXES.include?(prefix)
|
|
264
|
+
end
|
|
265
|
+
|
|
234
266
|
# XSD 1.0 §3.3.4 / §3.4.6: block on the element declaration
|
|
235
267
|
# AND on the declared complexType forbids replacing the type
|
|
236
268
|
# with one derived by the blocked kind. A type-less
|
|
@@ -241,15 +273,20 @@ module Lutaml
|
|
|
241
273
|
# @return [Boolean]
|
|
242
274
|
def override_blocked?(schema_element, override_type, repository)
|
|
243
275
|
declared_local = schema_element.type.to_s.split(":").last
|
|
244
|
-
|
|
276
|
+
kinds =
|
|
245
277
|
if declared_local.to_s.empty?
|
|
246
|
-
|
|
278
|
+
any_type_chain_kinds(override_type, repository)
|
|
247
279
|
else
|
|
248
|
-
derivation_kind_from(override_type, declared_local)
|
|
249
|
-
|
|
250
|
-
|
|
280
|
+
direct = derivation_kind_from(override_type, declared_local)
|
|
281
|
+
direct ||= member_derivation_kind(override_type,
|
|
282
|
+
declared_local,
|
|
283
|
+
repository)
|
|
284
|
+
direct ||= derivation_chain_kinds(override_type,
|
|
285
|
+
declared_local,
|
|
286
|
+
repository).first
|
|
287
|
+
Array(direct)
|
|
251
288
|
end
|
|
252
|
-
return false
|
|
289
|
+
return false if kinds.empty?
|
|
253
290
|
|
|
254
291
|
tokens = effective_block_tokens(schema_element, repository)
|
|
255
292
|
declared = repository&.resolve_named_type(declared_local)
|
|
@@ -261,7 +298,68 @@ module Lutaml
|
|
|
261
298
|
tokens.intersect?(%w[extension restriction])
|
|
262
299
|
|
|
263
300
|
blocked = tokens.include?("#all") ? %w[extension restriction] : tokens
|
|
264
|
-
blocked.include?(kind)
|
|
301
|
+
kinds.any? { |kind| blocked.include?(kind) }
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# §3.3.4 clause 2.2.2: the block applies to the derivation
|
|
305
|
+
# IN WHOLE OR IN PART — the kinds of EVERY hop from the
|
|
306
|
+
# override down to the declared type (ctI's indirect shapes).
|
|
307
|
+
#
|
|
308
|
+
# @return [Array<String>]
|
|
309
|
+
def derivation_chain_kinds(type, declared_local, repository,
|
|
310
|
+
depth = 0)
|
|
311
|
+
return [] if type.nil? || declared_local.to_s.empty? || depth > 10
|
|
312
|
+
|
|
313
|
+
case type
|
|
314
|
+
when Lutaml::Xml::Schema::Xsd::ComplexType
|
|
315
|
+
part = derivation_part(type)
|
|
316
|
+
return [] unless part
|
|
317
|
+
|
|
318
|
+
kind = part.class.name.include?("Extension") ? "extension" : "restriction"
|
|
319
|
+
next_local = part.base.to_s.split(":").last
|
|
320
|
+
return [kind] if next_local == declared_local
|
|
321
|
+
|
|
322
|
+
[kind, *derivation_chain_kinds(
|
|
323
|
+
repository&.resolve_named_type(next_local), declared_local,
|
|
324
|
+
repository, depth + 1
|
|
325
|
+
)]
|
|
326
|
+
when Lutaml::Xml::Schema::Xsd::SimpleType
|
|
327
|
+
restriction = type.restriction
|
|
328
|
+
return [] unless restriction
|
|
329
|
+
|
|
330
|
+
next_local = restriction.base.to_s.split(":").last
|
|
331
|
+
return ["restriction"] if next_local == declared_local
|
|
332
|
+
|
|
333
|
+
derivation_chain_kinds(
|
|
334
|
+
repository&.resolve_named_type(next_local), declared_local,
|
|
335
|
+
repository, depth + 1
|
|
336
|
+
)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Kinds along the override's whole chain down to the ur-type
|
|
341
|
+
# — a type-less declaration's block governs every hop
|
|
342
|
+
# (particlesIg003: ext extends base, base restricts anyType).
|
|
343
|
+
#
|
|
344
|
+
# @return [Array<String>]
|
|
345
|
+
def any_type_chain_kinds(type, repository, depth = 0)
|
|
346
|
+
return [] if type.nil? || depth > 10
|
|
347
|
+
|
|
348
|
+
if type.is_a?(Lutaml::Xml::Schema::Xsd::SimpleType)
|
|
349
|
+
return ["restriction"]
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
part = derivation_part(type)
|
|
353
|
+
# A bare complexType (no derivation marker) restricts the
|
|
354
|
+
# ur-type per §3.4.3.
|
|
355
|
+
return ["restriction"] unless part
|
|
356
|
+
|
|
357
|
+
kind = part.class.name.include?("Extension") ? "extension" : "restriction"
|
|
358
|
+
next_local = part.base.to_s.split(":").last
|
|
359
|
+
[kind, *any_type_chain_kinds(
|
|
360
|
+
repository&.resolve_named_type(next_local), repository,
|
|
361
|
+
depth + 1
|
|
362
|
+
)]
|
|
265
363
|
end
|
|
266
364
|
|
|
267
365
|
# Derivation kind of an override against a type-less
|
|
@@ -418,6 +516,95 @@ module Lutaml
|
|
|
418
516
|
"#{prefix}:#{local}"
|
|
419
517
|
end
|
|
420
518
|
|
|
519
|
+
# XSD 1.0 §3.3.4 clause 2.2: a builtin override owes valid
|
|
520
|
+
# derivation from the declared type. anyType derives from
|
|
521
|
+
# nothing but itself; every other builtin derives from
|
|
522
|
+
# anySimpleType (§4.1.2 ancestry table); a named declared
|
|
523
|
+
# type must BE the builtin or one of its ancestors
|
|
524
|
+
# (stZ073b: xs:integer is no derivation of the
|
|
525
|
+
# restriction-of-union st.unionType); a declared UNION admits
|
|
526
|
+
# overrides naming a member or a builtin derived from one
|
|
527
|
+
# (the elemT071 rule, builtin members included).
|
|
528
|
+
#
|
|
529
|
+
# @return [Boolean]
|
|
530
|
+
def builtin_override_undervalued?(xml_element, schema_element,
|
|
531
|
+
repository)
|
|
532
|
+
override = xsi_type_name(xml_element).to_s
|
|
533
|
+
declared = declared_local_name(schema_element)
|
|
534
|
+
return false if declared.empty? || declared == override
|
|
535
|
+
return true if override == ANY_TYPE_LOCAL
|
|
536
|
+
return false if declared == ANY_SIMPLE_TYPE_LOCAL
|
|
537
|
+
|
|
538
|
+
covered = ValueSpace.builtin_derivation_cover(declared,
|
|
539
|
+
override)
|
|
540
|
+
return !covered unless covered.nil?
|
|
541
|
+
|
|
542
|
+
declared_type = repository&.resolve_named_type(declared)
|
|
543
|
+
return false unless declared_type.is_a?(
|
|
544
|
+
Lutaml::Xml::Schema::Xsd::SimpleType,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
if declared_type.union
|
|
548
|
+
return !union_member_covers_builtin?(declared_type, override,
|
|
549
|
+
repository)
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
!validly_derived?(declared_type, override, repository)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# Whether the override builtin is admitted by a declared
|
|
556
|
+
# union: it names a member, or a member's chain reaches it.
|
|
557
|
+
#
|
|
558
|
+
# @return [Boolean]
|
|
559
|
+
def union_member_covers_builtin?(union_type, override, repository)
|
|
560
|
+
members = Array(union_type.union.member_types)
|
|
561
|
+
members.any? do |member|
|
|
562
|
+
local = member.to_s.split(":").last
|
|
563
|
+
next true if local == override
|
|
564
|
+
|
|
565
|
+
member_type = repository&.resolve_named_type(local)
|
|
566
|
+
next false unless member_type
|
|
567
|
+
|
|
568
|
+
validly_derived?(member_type, override, repository)
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# Handles an element whose xsi:type names a built-in: the
|
|
573
|
+
# §3.3.4 derivation gate, then builtin content validation.
|
|
574
|
+
#
|
|
575
|
+
# @return [Boolean] true when the element was handled
|
|
576
|
+
def builtin_override_handled?(xml_element, schema_element,
|
|
577
|
+
collector, repository)
|
|
578
|
+
if builtin_override_undervalued?(xml_element, schema_element,
|
|
579
|
+
repository)
|
|
580
|
+
report_error(
|
|
581
|
+
collector,
|
|
582
|
+
code: "xsi_type_not_derived",
|
|
583
|
+
message: "xsi:type '#{xsi_type_name(xml_element)}' is " \
|
|
584
|
+
"not validly derived from the declared type " \
|
|
585
|
+
"'#{declared_local_name(schema_element)}'",
|
|
586
|
+
location: xml_element.xpath,
|
|
587
|
+
context: { xsi_type: xsi_type_name(xml_element) },
|
|
588
|
+
)
|
|
589
|
+
return true
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
validate_builtin_type(xml_element,
|
|
593
|
+
builtin_override_reference(xml_element),
|
|
594
|
+
collector,
|
|
595
|
+
declared: schema_element)
|
|
596
|
+
true
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
# The declared type's local name ('' when the declaration is
|
|
600
|
+
# type-less — the implicit anyType).
|
|
601
|
+
#
|
|
602
|
+
# @param schema_element [Object]
|
|
603
|
+
# @return [String]
|
|
604
|
+
def declared_local_name(schema_element)
|
|
605
|
+
schema_element.type.to_s.split(":").last.to_s
|
|
606
|
+
end
|
|
607
|
+
|
|
421
608
|
# Resolve the type definition for an element
|
|
422
609
|
#
|
|
423
610
|
# @param schema_element [Lutaml::Xsd::Element, Lutaml::Xsd::ComplexType] Schema element
|
|
@@ -566,7 +753,8 @@ module Lutaml
|
|
|
566
753
|
# @param collector [ResultCollector] Result collector
|
|
567
754
|
# @return [void]
|
|
568
755
|
def validate_complex_type_content(xml_element, complex_type,
|
|
569
|
-
collector, repository: nil
|
|
756
|
+
collector, repository: nil,
|
|
757
|
+
declared: nil)
|
|
570
758
|
# Complex type validation is handled by:
|
|
571
759
|
# - AttributeValidationRule for attributes
|
|
572
760
|
# - ContentModelValidationRule for content model
|
|
@@ -575,7 +763,8 @@ module Lutaml
|
|
|
575
763
|
# Check for simple content
|
|
576
764
|
if complex_type.simple_content
|
|
577
765
|
validate_simple_content(xml_element, complex_type.simple_content,
|
|
578
|
-
collector, repository: repository
|
|
766
|
+
collector, repository: repository,
|
|
767
|
+
declared: declared)
|
|
579
768
|
end
|
|
580
769
|
end
|
|
581
770
|
|
|
@@ -1044,12 +1233,26 @@ module Lutaml
|
|
|
1044
1233
|
# @param collector [ResultCollector] Result collector
|
|
1045
1234
|
# @return [void]
|
|
1046
1235
|
def validate_simple_content(xml_element, simple_content, collector,
|
|
1047
|
-
repository: nil)
|
|
1236
|
+
repository: nil, declared: nil)
|
|
1048
1237
|
# Simple content means element has text content and attributes
|
|
1049
1238
|
# Validate the text content as a simple type
|
|
1050
1239
|
if simple_content.extension
|
|
1051
|
-
#
|
|
1052
|
-
#
|
|
1240
|
+
# The extension's TEXT still validates against the
|
|
1241
|
+
# extension's base type — the extension itself only adds
|
|
1242
|
+
# attributes (addB114: '62' violates base A's enumeration
|
|
1243
|
+
# and range through E-A's simpleContent extension).
|
|
1244
|
+
raw_base = simple_content.extension.base.to_s
|
|
1245
|
+
base_local = raw_base.split(":").last
|
|
1246
|
+
base_type = repository&.resolve_named_type(base_local)
|
|
1247
|
+
if base_type.is_a?(Lutaml::Xml::Schema::Xsd::SimpleType)
|
|
1248
|
+
validate_simple_type_content(xml_element, base_type,
|
|
1249
|
+
collector,
|
|
1250
|
+
repository: repository,
|
|
1251
|
+
declared: declared)
|
|
1252
|
+
elsif builtin_base?(raw_base)
|
|
1253
|
+
validate_builtin_type(xml_element, raw_base, collector,
|
|
1254
|
+
declared: declared)
|
|
1255
|
+
end
|
|
1053
1256
|
elsif simple_content.restriction
|
|
1054
1257
|
# Restriction constrains a simple type
|
|
1055
1258
|
value = xml_element.text_content
|
|
@@ -607,17 +607,28 @@ module Lutaml
|
|
|
607
607
|
# @param parent_declaration [Lutaml::Xml::Schema::Xsd::Element, nil]
|
|
608
608
|
# @param local_name [String]
|
|
609
609
|
# @return [Array<Lutaml::Xml::Schema::Xsd::Element>]
|
|
610
|
+
#
|
|
611
|
+
# Memoized per (declaration, local name): the dispatch calls
|
|
612
|
+
# this for every element, and the particle-tree walk cost is
|
|
613
|
+
# constant across elements of the same parent type (the schema
|
|
614
|
+
# is immutable once indexed).
|
|
610
615
|
def positional_children(parent_declaration, local_name)
|
|
616
|
+
key = [parent_declaration.object_id, local_name]
|
|
617
|
+
cache = (@positional_children ||= {})
|
|
618
|
+
cached = cache[key]
|
|
619
|
+
return cached if cached
|
|
620
|
+
|
|
611
621
|
type = parent_declaration&.complex_type ||
|
|
612
622
|
resolve_type(parent_declaration)
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
623
|
+
computed = []
|
|
624
|
+
if type.is_a?(Lutaml::Xml::Schema::Xsd::ComplexType)
|
|
625
|
+
particles = []
|
|
626
|
+
collect_named_particles(particle_for_type(type), local_name,
|
|
627
|
+
particles, {}.compare_by_identity, 0)
|
|
628
|
+
computed = particles if particles.all? { |p| @namespaces.key?(p) }
|
|
629
|
+
end
|
|
630
|
+
cache[key] = computed
|
|
631
|
+
computed
|
|
621
632
|
end
|
|
622
633
|
|
|
623
634
|
private
|
|
@@ -66,6 +66,79 @@ module Lutaml
|
|
|
66
66
|
|
|
67
67
|
BUILTIN_PREFIXES = %w[xs xsd].freeze
|
|
68
68
|
|
|
69
|
+
# The built-in type hierarchy (XSD 1.0 §4.1.2, Figure 4-1 +
|
|
70
|
+
# §3.14.6): each local name maps to its ANCESTOR builtins.
|
|
71
|
+
# A builtin xsi:type override is validly derived from a
|
|
72
|
+
# declared type only when the declared name is the override
|
|
73
|
+
# itself or one of its ancestors (§3.3.4 clause 2.2).
|
|
74
|
+
BUILTIN_ANCESTORS = {
|
|
75
|
+
"anyType" => [].freeze,
|
|
76
|
+
"anySimpleType" => %w[anyType].freeze,
|
|
77
|
+
"string" => %w[anySimpleType anyType].freeze,
|
|
78
|
+
"normalizedString" => %w[string anySimpleType anyType].freeze,
|
|
79
|
+
"token" => %w[normalizedString string anySimpleType anyType].freeze,
|
|
80
|
+
"language" => %w[token normalizedString string anySimpleType anyType].freeze,
|
|
81
|
+
"Name" => %w[token normalizedString string anySimpleType anyType].freeze,
|
|
82
|
+
"NCName" => %w[Name token normalizedString string anySimpleType anyType].freeze,
|
|
83
|
+
"ID" => %w[NCName Name token normalizedString string anySimpleType anyType].freeze,
|
|
84
|
+
"IDREF" => %w[NCName Name token normalizedString string anySimpleType anyType].freeze,
|
|
85
|
+
"NMTOKEN" => %w[token normalizedString string anySimpleType anyType].freeze,
|
|
86
|
+
"IDREFS" => %w[IDREF NCName Name token normalizedString string anySimpleType anyType].freeze,
|
|
87
|
+
"ENTITY" => %w[NCName Name token normalizedString string anySimpleType anyType].freeze,
|
|
88
|
+
"ENTITIES" => %w[ENTITY NCName Name token normalizedString string anySimpleType anyType].freeze,
|
|
89
|
+
"NMTOKENS" => %w[NMTOKEN token normalizedString string anySimpleType anyType].freeze,
|
|
90
|
+
"decimal" => %w[anySimpleType anyType].freeze,
|
|
91
|
+
"integer" => %w[decimal anySimpleType anyType].freeze,
|
|
92
|
+
"long" => %w[integer decimal anySimpleType anyType].freeze,
|
|
93
|
+
"int" => %w[long integer decimal anySimpleType anyType].freeze,
|
|
94
|
+
"short" => %w[int long integer decimal anySimpleType anyType].freeze,
|
|
95
|
+
"byte" => %w[short int long integer decimal anySimpleType anyType].freeze,
|
|
96
|
+
"nonNegativeInteger" => %w[integer decimal anySimpleType anyType].freeze,
|
|
97
|
+
"positiveInteger" => %w[nonNegativeInteger integer decimal anySimpleType anyType].freeze,
|
|
98
|
+
"unsignedLong" => %w[nonNegativeInteger integer decimal anySimpleType anyType].freeze,
|
|
99
|
+
"unsignedInt" => %w[unsignedLong nonNegativeInteger integer decimal anySimpleType anyType].freeze,
|
|
100
|
+
"unsignedShort" => %w[unsignedInt unsignedLong nonNegativeInteger integer decimal anySimpleType anyType].freeze,
|
|
101
|
+
"unsignedByte" => %w[unsignedShort unsignedInt unsignedLong nonNegativeInteger integer decimal anySimpleType anyType].freeze,
|
|
102
|
+
"nonPositiveInteger" => %w[integer decimal anySimpleType anyType].freeze,
|
|
103
|
+
"negativeInteger" => %w[nonPositiveInteger integer decimal anySimpleType anyType].freeze,
|
|
104
|
+
"float" => %w[anySimpleType anyType].freeze,
|
|
105
|
+
"double" => %w[anySimpleType anyType].freeze,
|
|
106
|
+
"duration" => %w[anySimpleType anyType].freeze,
|
|
107
|
+
"dateTime" => %w[anySimpleType anyType].freeze,
|
|
108
|
+
"time" => %w[anySimpleType anyType].freeze,
|
|
109
|
+
"date" => %w[anySimpleType anyType].freeze,
|
|
110
|
+
"gYear" => %w[anySimpleType anyType].freeze,
|
|
111
|
+
"gYearMonth" => %w[anySimpleType anyType].freeze,
|
|
112
|
+
"gMonth" => %w[anySimpleType anyType].freeze,
|
|
113
|
+
"gMonthDay" => %w[anySimpleType anyType].freeze,
|
|
114
|
+
"gDay" => %w[anySimpleType anyType].freeze,
|
|
115
|
+
"boolean" => %w[anySimpleType anyType].freeze,
|
|
116
|
+
"base64Binary" => %w[anySimpleType anyType].freeze,
|
|
117
|
+
"hexBinary" => %w[anySimpleType anyType].freeze,
|
|
118
|
+
"anyURI" => %w[anySimpleType anyType].freeze,
|
|
119
|
+
"QName" => %w[anySimpleType anyType].freeze,
|
|
120
|
+
"NOTATION" => %w[QName anySimpleType anyType].freeze,
|
|
121
|
+
}.freeze
|
|
122
|
+
|
|
123
|
+
# Whether a builtin override local is validly derived from the
|
|
124
|
+
# declared local: identity or the declared name sits in the
|
|
125
|
+
# override's builtin ancestry. TRI-STATE: nil when the override
|
|
126
|
+
# is not a builtin local at all (the caller falls through to
|
|
127
|
+
# schema-type resolution). No `?` suffix — rubocop -A rewrites
|
|
128
|
+
# nil-returning predicate guards to false and breaks the
|
|
129
|
+
# contract.
|
|
130
|
+
#
|
|
131
|
+
# @param declared [String] declared type local name
|
|
132
|
+
# @param override [String] override builtin local name
|
|
133
|
+
# @return [Boolean, nil]
|
|
134
|
+
def self.builtin_derivation_cover(declared, override)
|
|
135
|
+
ancestors = BUILTIN_ANCESTORS[override.to_s]
|
|
136
|
+
return nil unless ancestors
|
|
137
|
+
|
|
138
|
+
declared.to_s == override.to_s ||
|
|
139
|
+
ancestors.include?(declared.to_s)
|
|
140
|
+
end
|
|
141
|
+
|
|
69
142
|
# xsd:length counts list ITEMS for list types, not characters.
|
|
70
143
|
LIST_BASES = %w[IDREFS ENTITIES NMTOKENS].freeze
|
|
71
144
|
|
data/lib/lutaml/xsd/version.rb
CHANGED