lutaml-xsd 1.2.2 → 1.2.4
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/Gemfile.lock +2 -2
- data/README.adoc +22 -0
- data/TODO.conformance.md +17 -7
- data/docs/CONFORMANCE.adoc +11 -3
- data/lib/lutaml/xsd/validation/rules/content_model_validation_rule.rb +124 -4
- data/lib/lutaml/xsd/validation/rules/occurrence_validation_rule.rb +27 -4
- data/lib/lutaml/xsd/validation/schema_definition_resolver.rb +23 -1
- data/lib/lutaml/xsd/validation/unique_particle_attribution.rb +58 -15
- data/lib/lutaml/xsd/validation/validation_job.rb +29 -0
- data/lib/lutaml/xsd/validation/validation_rule.rb +17 -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: da77a9a03a7bdbb0a9ea2092de4634b5151a6bafe0781592d58fbf869aacb542
|
|
4
|
+
data.tar.gz: 11c964841aa88464f5ab94c176f18757b1ff5bdc33813030d24cb7429957b828
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 96be4d45cabc55c0e0839a456ce4d6948cbe326bc25d72e1f5f228ead717a4a578871295f3b1bbeb9714657997217345657ba2c0f1b9af8a502211d62e9748cd
|
|
7
|
+
data.tar.gz: 55c6018abace93a782f1d661706568aeb9c48ed3c2c88b31fc64504a6586bb5ae3661fbdcbf312f0ffe75fd3e782e8c11c392387f54548bcbaab19be72892714
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
lutaml-xsd (1.2.
|
|
4
|
+
lutaml-xsd (1.2.3)
|
|
5
5
|
liquid (>= 4.0, < 6.0)
|
|
6
6
|
lutaml-model (~> 0.8.123)
|
|
7
7
|
lutaml-store (~> 0.3)
|
|
@@ -264,7 +264,7 @@ CHECKSUMS
|
|
|
264
264
|
liquid (5.14.0) sha256=e1020c2b2ee58c7a4d3bfdc7a331537bd27a783353748bff0bf27c6ac0c17065
|
|
265
265
|
lutaml-model (0.8.123) sha256=475f391371965f5baee2a9d018a7c9da8361c3774d3802749aa8805ee1253fde
|
|
266
266
|
lutaml-store (0.3.2) sha256=9c455dbab32b4c635c48a27213f6f56fda191731c4574eb6187184ba0588a672
|
|
267
|
-
lutaml-xsd (1.2.
|
|
267
|
+
lutaml-xsd (1.2.3)
|
|
268
268
|
memo_wise (1.13.0) sha256=30220c38c4cef410849bc73553c58664dc2c91c6379e4a1df22aea02358b716b
|
|
269
269
|
moxml (0.5.153) sha256=561c6df843edd4a3b7f4c6785f9cfd6fef61050df7f1113fdcd45d8495704fc2
|
|
270
270
|
nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f
|
data/README.adoc
CHANGED
|
@@ -339,6 +339,28 @@ automatic detection of version-specific features. Validation is automatically
|
|
|
339
339
|
enabled when parsing schemas via [`Lutaml::Xsd.parse()`](lib/lutaml/xsd.rb:47) but can be disabled
|
|
340
340
|
if needed.
|
|
341
341
|
|
|
342
|
+
=== Instance validation
|
|
343
|
+
|
|
344
|
+
Beyond schema-document checks, the `Lutaml::Xsd::Validation` module validates
|
|
345
|
+
XML *instances* against parsed schemas: element structure and content models
|
|
346
|
+
(sequences, choices, nested groups, `xs:any` wildcards with namespace
|
|
347
|
+
constraints and `processContents` enforcement), attribute presence, typing and
|
|
348
|
+
`xs:anyAttribute` on every shape, builtin/simple-type facets, `xsi:nil`
|
|
349
|
+
handling, `xsi:type` dynamic resolution, key/keyref/unique identity
|
|
350
|
+
constraints, and schema-side Unique Particle Attribution checking via
|
|
351
|
+
`Lutaml::Xsd::Validation::UniqueParticleAttribution`.
|
|
352
|
+
|
|
353
|
+
The pipeline passes 42/42 checks of its conformance corpus
|
|
354
|
+
(`rake conformance`); measured results live in `docs/CONFORMANCE.adoc` and
|
|
355
|
+
`TODO.conformance.md`.
|
|
356
|
+
|
|
357
|
+
[NOTE]
|
|
358
|
+
====
|
|
359
|
+
Guaranteed feature set requires `lutaml-model >= 0.8.123` (the gemspec
|
|
360
|
+
floor). Keyref resolution and complexType/extension `xs:anyAttribute`
|
|
361
|
+
checking depend on model mappings introduced there.
|
|
362
|
+
====
|
|
363
|
+
|
|
342
364
|
=== Validation checks
|
|
343
365
|
|
|
344
366
|
The schema validator performs these checks:
|
data/TODO.conformance.md
CHANGED
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
Goal: conformance is a number, not a feeling. Vendor the W3C XML Schema
|
|
4
4
|
test-suite categories, build the harness, publish the pass-rate, grow it.
|
|
5
5
|
|
|
6
|
-
## Baseline (2026-10-
|
|
6
|
+
## Baseline (2026-10-23, 22 cases / 47 checks, full pass)
|
|
7
7
|
|
|
8
|
-
Corpus:
|
|
8
|
+
Corpus: 22 case classes under `spec/conformance/cases/`, modelled on the
|
|
9
9
|
W3C XML Schema Test Suite category taxonomy — self-authored because the
|
|
10
10
|
canonical 2007 suite distribution is no longer served from w3.org (owner
|
|
11
11
|
can vendor a tarball later; provenance README goes with it).
|
|
12
12
|
|
|
13
13
|
| mode | pass-rate |
|
|
14
14
|
|---|---|
|
|
15
|
-
| engine (Moxml::XSD tier-1, leptris 1.9.380.2) | 38/42 (90.5
|
|
16
|
-
| pipeline (ValidationJob + wired rule engine) |
|
|
15
|
+
| engine (Moxml::XSD tier-1, leptris 1.9.380.2) | 38/42 (90.5%, 2 cases unsupported) |
|
|
16
|
+
| pipeline (ValidationJob + wired rule engine) | 47/47 (100.0%) |
|
|
17
17
|
|
|
18
18
|
The pipeline now passes every check in the corpus. The engine trails on
|
|
19
19
|
four: strict wildcard/anyAttribute under-detection, xsi:nil on
|
|
@@ -27,7 +27,9 @@ History: 2026-10-20 baseline 20/25 (80.0%) with pipeline == engine
|
|
|
27
27
|
baseline 34/39 (87.2%); gap rules 37/39 (94.9%); anyAttribute 40/42
|
|
28
28
|
(95.2%); keyref activation via lutaml-model 0.8.121 → 41/42 (97.6%);
|
|
29
29
|
lutaml-model 0.8.123 (complexType/extension anyAttribute mappings) +
|
|
30
|
-
leptris 1.9.380.2 → 42/42 (100.0%)
|
|
30
|
+
leptris 1.9.380.2 → 42/42 (100.0%); xs:group expansion in the content
|
|
31
|
+
and occurrence walks grew the corpus to 45 checks → 45/45 (100.0%);
|
|
32
|
+
abstract-element usage checking → 47/47 (100.0%).
|
|
31
33
|
|
|
32
34
|
Remaining engine-mode gaps (tracked, moxml/leptris-side):
|
|
33
35
|
|
|
@@ -95,7 +97,15 @@ Remaining engine-mode gaps (tracked, moxml/leptris-side):
|
|
|
95
97
|
overlap, choice-alternative and sequence-junction conflict rules,
|
|
96
98
|
container occurrence folding. Standalone API
|
|
97
99
|
(`findings` → upa_violation findings); zero findings on the
|
|
98
|
-
conformance corpus schemas (no false positives).
|
|
99
|
-
xs:group
|
|
100
|
+
conformance corpus schemas (no false positives). Depth (2026-10-22):
|
|
101
|
+
xs:group references expand to the referenced declaration
|
|
102
|
+
(cycle-guarded), and element first-sets include substitution-group
|
|
103
|
+
members — head/member junctions report per XSD 1.0 while required
|
|
104
|
+
head/member sequences stay deterministic. Wired into
|
|
105
|
+
`ValidationJob` as an opt-out `validate_upa` phase: schema
|
|
106
|
+
findings surface as warnings and never flip instance validity.
|
|
107
|
+
Also fixed the occurrence rule's choice counting: one instance
|
|
108
|
+
element matching duplicate alternatives counts once, not once
|
|
109
|
+
per alternative
|
|
100
110
|
- [ ] XSD 1.1 constructs behind a version flag — DEFERRED: 1.1 strategy
|
|
101
111
|
decision needed (see TODO.som.md)
|
data/docs/CONFORMANCE.adoc
CHANGED
|
@@ -32,9 +32,17 @@ Two modes are measured independently:
|
|
|
32
32
|
|
|
33
33
|
## Baseline and gap register
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
Current measured result (2026-10-23, 20 case classes / 42 checks):
|
|
36
|
+
|
|
37
|
+
- **pipeline** — 42/42 (100.0%)
|
|
38
|
+
- **engine** — 38/42 (90.5%); the four remaining are documented tier-1
|
|
39
|
+
laxness (strict wildcard and anyAttribute under-detection, `xsi:nil` on
|
|
40
|
+
non-nillable elements, wrong-namespace root matching), tracked in
|
|
41
|
+
lutaml/moxml#452
|
|
42
|
+
|
|
43
|
+
The full history and gap register live in `TODO.conformance.md`; each
|
|
44
|
+
failing check maps to a tracked gap, and the register is the work queue
|
|
45
|
+
for the conformance pillar.
|
|
38
46
|
|
|
39
47
|
## Adding a case
|
|
40
48
|
|
|
@@ -59,7 +59,7 @@ module Lutaml
|
|
|
59
59
|
def validate(xml_element, schema_def, collector, repository: nil)
|
|
60
60
|
return unless schema_def
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
handled = engine_content_check(xml_element, collector) == :handled
|
|
63
63
|
|
|
64
64
|
particles = effective_particles(
|
|
65
65
|
xml_element, schema_def, repository
|
|
@@ -68,6 +68,12 @@ module Lutaml
|
|
|
68
68
|
# rule already reports it, so no content walk runs here.
|
|
69
69
|
return if particles.nil? || particles.empty?
|
|
70
70
|
|
|
71
|
+
# The engine does not check abstract-element usage; scan for
|
|
72
|
+
# it regardless of whether the engine answered.
|
|
73
|
+
scan_abstract_usage(xml_element, particles, collector,
|
|
74
|
+
repository)
|
|
75
|
+
return if handled
|
|
76
|
+
|
|
71
77
|
validate_particle_sequence(xml_element, particles, collector,
|
|
72
78
|
repository)
|
|
73
79
|
end
|
|
@@ -129,6 +135,10 @@ module Lutaml
|
|
|
129
135
|
# occurrence constraints survive the walk.
|
|
130
136
|
if particle.is_a?(Lutaml::Xsd::Sequence)
|
|
131
137
|
particles.concat(collect_particles(particle))
|
|
138
|
+
elsif particle.is_a?(Lutaml::Xml::Schema::Xsd::Group)
|
|
139
|
+
particles.concat(
|
|
140
|
+
expand_group_container(particle, repository),
|
|
141
|
+
)
|
|
132
142
|
else
|
|
133
143
|
particles << particle
|
|
134
144
|
end
|
|
@@ -161,6 +171,48 @@ module Lutaml
|
|
|
161
171
|
particles
|
|
162
172
|
end
|
|
163
173
|
|
|
174
|
+
# Engine-accepted content still needs the checks the engine
|
|
175
|
+
# lacks: abstract element declarations cannot appear in
|
|
176
|
+
# instances. Containers flatten to their leaf declarations —
|
|
177
|
+
# the walk keeps them whole, the scan needs the leaves.
|
|
178
|
+
def scan_abstract_usage(xml_element, particles, collector,
|
|
179
|
+
repository)
|
|
180
|
+
leaves = leaf_particles(particles, repository)
|
|
181
|
+
xml_element.children.each do |child|
|
|
182
|
+
leaves.each do |particle|
|
|
183
|
+
next unless particle.is_a?(Lutaml::Xsd::Element)
|
|
184
|
+
next unless particle_matches?(child, particle, repository,
|
|
185
|
+
parent: xml_element)
|
|
186
|
+
next unless element_declaration_abstract?(particle, child,
|
|
187
|
+
repository)
|
|
188
|
+
|
|
189
|
+
report_error(
|
|
190
|
+
collector,
|
|
191
|
+
code: "abstract_element_used",
|
|
192
|
+
message: "Element '#{child.qualified_name}' is " \
|
|
193
|
+
"abstract and cannot appear in the instance",
|
|
194
|
+
location: child.xpath,
|
|
195
|
+
context: { element: child.qualified_name },
|
|
196
|
+
)
|
|
197
|
+
break
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def leaf_particles(particles, repository)
|
|
203
|
+
particles.flat_map do |particle|
|
|
204
|
+
case particle
|
|
205
|
+
when Lutaml::Xml::Schema::Xsd::Group
|
|
206
|
+
leaf_particles(expand_group_container(particle, repository),
|
|
207
|
+
repository)
|
|
208
|
+
when Lutaml::Xsd::Sequence, Lutaml::Xsd::Choice, Lutaml::Xsd::All
|
|
209
|
+
leaf_particles(collect_particles(particle), repository)
|
|
210
|
+
else
|
|
211
|
+
[particle]
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
164
216
|
# Sequential walk over a particle list (min/max per particle,
|
|
165
217
|
# recursive matching, wildcard permission for unmatched tails).
|
|
166
218
|
#
|
|
@@ -181,7 +233,8 @@ module Lutaml
|
|
|
181
233
|
# enforced loosely for the same reason.
|
|
182
234
|
container = particle.is_a?(Lutaml::Xsd::Sequence) ||
|
|
183
235
|
particle.is_a?(Lutaml::Xsd::Choice) ||
|
|
184
|
-
particle.is_a?(Lutaml::Xsd::All)
|
|
236
|
+
particle.is_a?(Lutaml::Xsd::All) ||
|
|
237
|
+
particle.is_a?(Lutaml::Xml::Schema::Xsd::Group)
|
|
185
238
|
max_occurs = parse_max_occurs(
|
|
186
239
|
particle.max_occurs, container ? :unbounded : 1
|
|
187
240
|
)
|
|
@@ -197,6 +250,16 @@ module Lutaml
|
|
|
197
250
|
if particle.is_a?(Lutaml::Xsd::Any)
|
|
198
251
|
check_strict_wildcard_child(child, particle, collector,
|
|
199
252
|
repository)
|
|
253
|
+
elsif particle.is_a?(Lutaml::Xsd::Element) &&
|
|
254
|
+
element_declaration_abstract?(particle, child, repository)
|
|
255
|
+
report_error(
|
|
256
|
+
collector,
|
|
257
|
+
code: "abstract_element_used",
|
|
258
|
+
message: "Element '#{child.qualified_name}' is " \
|
|
259
|
+
"abstract and cannot appear in the instance",
|
|
260
|
+
location: child.xpath,
|
|
261
|
+
context: { element: child.qualified_name },
|
|
262
|
+
)
|
|
200
263
|
end
|
|
201
264
|
|
|
202
265
|
matched_count += 1
|
|
@@ -244,6 +307,30 @@ module Lutaml
|
|
|
244
307
|
end
|
|
245
308
|
end
|
|
246
309
|
|
|
310
|
+
# Whether the matched declaration is abstract: abstract element
|
|
311
|
+
# declarations cannot appear directly in instances (substitution
|
|
312
|
+
# members remain legal). Direct declarations carry their own
|
|
313
|
+
# abstract flag; references resolve to the global declaration.
|
|
314
|
+
def element_declaration_abstract?(particle, child, repository)
|
|
315
|
+
declaration = declaration_for(particle, child, repository)
|
|
316
|
+
return false unless declaration
|
|
317
|
+
|
|
318
|
+
truthy?(declaration.abstract) && child.name == declaration.name
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def declaration_for(particle, child, repository)
|
|
322
|
+
return particle if particle.name.to_s.match?(/\S/)
|
|
323
|
+
return nil unless repository
|
|
324
|
+
|
|
325
|
+
local = particle.ref.to_s.split(":").last
|
|
326
|
+
repository.resolve(local, child.namespace_uri) ||
|
|
327
|
+
repository.resolve(local, nil)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def truthy?(value)
|
|
331
|
+
value.to_s == "true"
|
|
332
|
+
end
|
|
333
|
+
|
|
247
334
|
# Report an unsatisfied minimum occurrence: a choice container
|
|
248
335
|
# names its alternatives, anything else reports generically.
|
|
249
336
|
#
|
|
@@ -429,20 +516,28 @@ module Lutaml
|
|
|
429
516
|
# wildcard namespace constraints scoped to the target namespace
|
|
430
517
|
# @return [Boolean]
|
|
431
518
|
def particle_matches?(xml_element, particle, repository = nil,
|
|
432
|
-
parent: nil)
|
|
519
|
+
parent: nil, group_depth: 0)
|
|
433
520
|
case particle
|
|
434
521
|
when Lutaml::Xsd::Element
|
|
435
522
|
element_matches?(xml_element, particle, repository)
|
|
436
523
|
when Lutaml::Xsd::Any
|
|
437
524
|
wildcard_permits?(particle, parent || xml_element,
|
|
438
525
|
xml_element, repository)
|
|
526
|
+
when Lutaml::Xml::Schema::Xsd::Group
|
|
527
|
+
expand_group_container(particle, repository,
|
|
528
|
+
group_depth).any? do |container|
|
|
529
|
+
particle_matches?(xml_element, container, repository,
|
|
530
|
+
parent: parent,
|
|
531
|
+
group_depth: group_depth + 1)
|
|
532
|
+
end
|
|
439
533
|
when Lutaml::Xsd::Sequence, Lutaml::Xsd::Choice, Lutaml::Xsd::All
|
|
440
534
|
# Nested content model: a child matches the container when it
|
|
441
535
|
# matches any leaf particle inside it. Inner ordering is
|
|
442
536
|
# permissive; the outer walk stays sequential.
|
|
443
537
|
collect_particles(particle).any? do |leaf|
|
|
444
538
|
particle_matches?(xml_element, leaf, repository,
|
|
445
|
-
parent: parent
|
|
539
|
+
parent: parent,
|
|
540
|
+
group_depth: group_depth)
|
|
446
541
|
end
|
|
447
542
|
else
|
|
448
543
|
false
|
|
@@ -458,6 +553,13 @@ module Lutaml
|
|
|
458
553
|
# @param repository [SchemaDefinitionResolver, nil]
|
|
459
554
|
# @return [Boolean]
|
|
460
555
|
def element_matches?(xml_element, schema_element, repository = nil)
|
|
556
|
+
local_ref = schema_element.name.to_s.empty? && schema_element.ref
|
|
557
|
+
if local_ref && repository
|
|
558
|
+
return ref_element_matches?(xml_element, schema_element,
|
|
559
|
+
local_ref.to_s.split(":").last,
|
|
560
|
+
repository)
|
|
561
|
+
end
|
|
562
|
+
|
|
461
563
|
if xml_element.name == schema_element.name
|
|
462
564
|
return namespaces_match_instance?(
|
|
463
565
|
xml_element,
|
|
@@ -468,6 +570,24 @@ module Lutaml
|
|
|
468
570
|
substitution_member?(xml_element, schema_element, repository)
|
|
469
571
|
end
|
|
470
572
|
|
|
573
|
+
# An xs:ref particle takes the name and target namespace of the
|
|
574
|
+
# global declaration it references; substitution members of that
|
|
575
|
+
# declaration match too.
|
|
576
|
+
def ref_element_matches?(xml_element, schema_element, local,
|
|
577
|
+
repository)
|
|
578
|
+
declaration = repository.resolve(local,
|
|
579
|
+
xml_element.namespace_uri) ||
|
|
580
|
+
repository.resolve(local, nil)
|
|
581
|
+
return false unless declaration
|
|
582
|
+
|
|
583
|
+
return true if xml_element.name == declaration.name &&
|
|
584
|
+
namespaces_match_instance?(
|
|
585
|
+
xml_element, repository.target_namespace_for(declaration)
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
substitution_member?(xml_element, schema_element, repository)
|
|
589
|
+
end
|
|
590
|
+
|
|
471
591
|
# Whether the instance element's declaration is in the
|
|
472
592
|
# substitution group headed by the particle's name/ref.
|
|
473
593
|
#
|
|
@@ -65,6 +65,11 @@ module Lutaml
|
|
|
65
65
|
when Lutaml::Xsd::All
|
|
66
66
|
validate_all_occurrences(parent_element, schema_particle,
|
|
67
67
|
collector, repository: repository)
|
|
68
|
+
when Lutaml::Xml::Schema::Xsd::Group
|
|
69
|
+
expand_group_container(schema_particle, repository).each do |container|
|
|
70
|
+
validate(parent_element, container, collector,
|
|
71
|
+
repository: repository)
|
|
72
|
+
end
|
|
68
73
|
end
|
|
69
74
|
end
|
|
70
75
|
|
|
@@ -265,15 +270,33 @@ module Lutaml
|
|
|
265
270
|
# @return [Integer]
|
|
266
271
|
def count_choice_alternatives(parent_element, choice,
|
|
267
272
|
repository = nil)
|
|
268
|
-
return 0 if choice.element.empty?
|
|
273
|
+
return 0 if choice.element.empty? || parent_element.children.empty?
|
|
269
274
|
|
|
270
275
|
alternatives = Array(choice.element)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
276
|
+
# Count instance elements that matched the choice — a single
|
|
277
|
+
# child matching several identical alternatives counts once.
|
|
278
|
+
parent_element.children.count do |child|
|
|
279
|
+
alternatives.any? do |alt_element|
|
|
280
|
+
count_element_occurrences_for_child(child, alt_element,
|
|
281
|
+
parent_element,
|
|
282
|
+
repository)
|
|
283
|
+
end
|
|
274
284
|
end
|
|
275
285
|
end
|
|
276
286
|
|
|
287
|
+
# Whether one instance child matches one alternative declaration.
|
|
288
|
+
def count_element_occurrences_for_child(child, schema_element,
|
|
289
|
+
_parent_element,
|
|
290
|
+
repository)
|
|
291
|
+
element_name = schema_element.name ||
|
|
292
|
+
schema_element.ref.to_s.split(":").last
|
|
293
|
+
target_ns = resolve_target_namespace(schema_element, repository)
|
|
294
|
+
|
|
295
|
+
(child.name == element_name &&
|
|
296
|
+
namespaces_match?(child.namespace_uri, target_ns)) ||
|
|
297
|
+
substitution_member?(child, schema_element, repository)
|
|
298
|
+
end
|
|
299
|
+
|
|
277
300
|
# Validate minimum occurrence constraint
|
|
278
301
|
#
|
|
279
302
|
# @param parent_element [XmlElement] Parent element
|
|
@@ -25,6 +25,7 @@ module Lutaml
|
|
|
25
25
|
%i[sequence choice all],
|
|
26
26
|
Lutaml::Xml::Schema::Xsd::RestrictionComplexContent =>
|
|
27
27
|
%i[sequence choice all],
|
|
28
|
+
Lutaml::Xml::Schema::Xsd::Group => %i[sequence choice all],
|
|
28
29
|
Lutaml::Xml::Schema::Xsd::Sequence => %i[sequence choice],
|
|
29
30
|
Lutaml::Xml::Schema::Xsd::Choice => %i[sequence choice],
|
|
30
31
|
Lutaml::Xml::Schema::Xsd::All => [].freeze,
|
|
@@ -123,6 +124,21 @@ module Lutaml
|
|
|
123
124
|
candidates.length == 1 ? candidates.first.first : nil
|
|
124
125
|
end
|
|
125
126
|
|
|
127
|
+
# Named group declaration by local name (for xs:group reference
|
|
128
|
+
# expansion in the content and occurrence walks).
|
|
129
|
+
#
|
|
130
|
+
# @param local_name [String]
|
|
131
|
+
# @return [Lutaml::Xml::Schema::Xsd::Group, nil]
|
|
132
|
+
def resolve_group(local_name)
|
|
133
|
+
return nil if local_name.nil? || local_name.empty?
|
|
134
|
+
|
|
135
|
+
schemas.each do |schema|
|
|
136
|
+
found = Array(schema.group).find { |g| g.name == local_name }
|
|
137
|
+
return found if found
|
|
138
|
+
end
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
126
142
|
# Content-model particle of the element's type, for occurrence
|
|
127
143
|
# and content-model rules.
|
|
128
144
|
#
|
|
@@ -147,7 +163,8 @@ module Lutaml
|
|
|
147
163
|
case type
|
|
148
164
|
when Lutaml::Xml::Schema::Xsd::ComplexType
|
|
149
165
|
direct = Array(type.sequence).first ||
|
|
150
|
-
Array(type.choice).first || Array(type.all).first
|
|
166
|
+
Array(type.choice).first || Array(type.all).first ||
|
|
167
|
+
Array(type.group).first
|
|
151
168
|
return direct if direct
|
|
152
169
|
|
|
153
170
|
# Derived types keep their particles under complex_content.
|
|
@@ -202,6 +219,11 @@ module Lutaml
|
|
|
202
219
|
Array(schema.attribute).each do |attribute|
|
|
203
220
|
index_attribute(attribute, namespace)
|
|
204
221
|
end
|
|
222
|
+
Array(schema.group).each do |group|
|
|
223
|
+
each_particle_element(group) do |element|
|
|
224
|
+
index_element(index, element, namespace, :inline)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
205
227
|
end
|
|
206
228
|
|
|
207
229
|
def index_attribute(attribute, namespace)
|
|
@@ -65,6 +65,17 @@ module Lutaml
|
|
|
65
65
|
def initialize(schema)
|
|
66
66
|
@schema = schema
|
|
67
67
|
@seen = {}.compare_by_identity
|
|
68
|
+
@groups = {}
|
|
69
|
+
Array(@schema.group).each do |group|
|
|
70
|
+
@groups[group.name.to_s] = group if group.name
|
|
71
|
+
end
|
|
72
|
+
@substitutes = {}
|
|
73
|
+
Array(@schema.element).each do |element|
|
|
74
|
+
next unless element.name && element.substitution_group.to_s.match?(/\S/)
|
|
75
|
+
|
|
76
|
+
head = element.substitution_group.to_s.split(":").last
|
|
77
|
+
(@substitutes[head] ||= []) << element
|
|
78
|
+
end
|
|
68
79
|
end
|
|
69
80
|
|
|
70
81
|
# @return [Array<Finding>] UPA violations, empty when deterministic
|
|
@@ -167,7 +178,7 @@ module Lutaml
|
|
|
167
178
|
def first_set(particle)
|
|
168
179
|
case particle
|
|
169
180
|
when Lutaml::Xml::Schema::Xsd::Element
|
|
170
|
-
FirstSet.new(
|
|
181
|
+
FirstSet.new(element_keys(particle), [].freeze,
|
|
171
182
|
parse_min(particle).zero?).tap do |first|
|
|
172
183
|
first.repeatable = parse_max(particle) != 1
|
|
173
184
|
end
|
|
@@ -234,21 +245,53 @@ module Lutaml
|
|
|
234
245
|
|
|
235
246
|
# Particles in declaration order — resolved_element_order preserves
|
|
236
247
|
# the source sequence including xs:any, which attribute-style
|
|
237
|
-
# accessor reads would reorder.
|
|
238
|
-
|
|
248
|
+
# accessor reads would reorder. xs:group references expand to the
|
|
249
|
+
# referenced declaration's particles (cycle-guarded per chain).
|
|
250
|
+
def ordered_particles(container, group_chain = {})
|
|
239
251
|
ordered = container.resolved_element_order
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
+
raw = if ordered.to_a.any?
|
|
253
|
+
ordered.to_a
|
|
254
|
+
else
|
|
255
|
+
case container
|
|
256
|
+
when Lutaml::Xml::Schema::Xsd::Sequence,
|
|
257
|
+
Lutaml::Xml::Schema::Xsd::Choice
|
|
258
|
+
%i[element any sequence choice].flat_map do |kind|
|
|
259
|
+
Array(container.public_send(kind))
|
|
260
|
+
end.compact
|
|
261
|
+
when Lutaml::Xml::Schema::Xsd::All
|
|
262
|
+
Array(container.element).compact
|
|
263
|
+
else
|
|
264
|
+
[]
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
raw.flat_map do |particle|
|
|
268
|
+
if particle.is_a?(Lutaml::Xml::Schema::Xsd::Group)
|
|
269
|
+
expand_group(particle, group_chain)
|
|
270
|
+
else
|
|
271
|
+
[particle]
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def expand_group(group_ref, chain)
|
|
277
|
+
local = (group_ref.ref || group_ref.name).to_s.split(":").last
|
|
278
|
+
declaration = @groups[local]
|
|
279
|
+
return [] unless declaration
|
|
280
|
+
return [] if chain[local]
|
|
281
|
+
|
|
282
|
+
chain[local] = true
|
|
283
|
+
ordered_particles(declaration, chain)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# An element particle matches its own name plus every member of
|
|
287
|
+
# the substitution group headed by it.
|
|
288
|
+
def element_keys(element)
|
|
289
|
+
own = element_key(element)
|
|
290
|
+
return [] unless own
|
|
291
|
+
|
|
292
|
+
members = @substitutes.fetch(own[1], [])
|
|
293
|
+
[own] + members.filter_map do |member|
|
|
294
|
+
member.name ? [@schema.target_namespace.to_s, member.name] : nil
|
|
252
295
|
end
|
|
253
296
|
end
|
|
254
297
|
|
|
@@ -5,6 +5,7 @@ require_relative "validation_error"
|
|
|
5
5
|
require_relative "xml_navigator"
|
|
6
6
|
require_relative "result_collector"
|
|
7
7
|
require_relative "rule_engine"
|
|
8
|
+
require_relative "unique_particle_attribution"
|
|
8
9
|
|
|
9
10
|
module Lutaml
|
|
10
11
|
module Xsd
|
|
@@ -62,6 +63,7 @@ module Lutaml
|
|
|
62
63
|
|
|
63
64
|
validate_root_declaration
|
|
64
65
|
run_engine_validation if @config.feature_enabled?(:engine_validation)
|
|
66
|
+
run_upa_validation if @config.feature_enabled?(:validate_upa)
|
|
65
67
|
validate_structure if @config.feature_enabled?(:validate_types)
|
|
66
68
|
return result_from_collector if should_stop_after_structure?
|
|
67
69
|
|
|
@@ -119,6 +121,33 @@ module Lutaml
|
|
|
119
121
|
# Stop validation if configured to stop on first error
|
|
120
122
|
end
|
|
121
123
|
|
|
124
|
+
# Schema-side UPA checking (XSD 1.0 3.8.6). Findings describe the
|
|
125
|
+
# schema rather than the instance, so they surface as warnings.
|
|
126
|
+
#
|
|
127
|
+
# @return [void]
|
|
128
|
+
def run_upa_validation
|
|
129
|
+
@repository.all_schemas.each_value do |schema|
|
|
130
|
+
UniqueParticleAttribution.new(schema).findings.each do |finding|
|
|
131
|
+
@result_collector.add_warning(
|
|
132
|
+
ValidationError.new(
|
|
133
|
+
code: finding.code,
|
|
134
|
+
message: finding.message,
|
|
135
|
+
severity: :warning,
|
|
136
|
+
context: { schema_path: finding.path },
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
rescue StandardError => e
|
|
142
|
+
@result_collector.add_warning(
|
|
143
|
+
ValidationError.new(
|
|
144
|
+
code: "upa_validation_unavailable",
|
|
145
|
+
message: "UPA validation skipped: #{e.message}",
|
|
146
|
+
severity: :warning,
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
122
151
|
# Run engine-native XSD validation (Moxml::XSD) when the adapter
|
|
123
152
|
# supports it. Findings are additive to rule findings.
|
|
124
153
|
#
|
|
@@ -210,6 +210,23 @@ module Lutaml
|
|
|
210
210
|
|
|
211
211
|
protected
|
|
212
212
|
|
|
213
|
+
# The container an xs:group reference expands to (the declaration's
|
|
214
|
+
# sequence/choice/all), kept whole — matching descends into it.
|
|
215
|
+
# XSD 1.0 forbids recursive groups; the depth guard only defends
|
|
216
|
+
# against malformed schemas.
|
|
217
|
+
def expand_group_container(group, repository, depth = 0)
|
|
218
|
+
return [] unless repository
|
|
219
|
+
return [] if depth > 5
|
|
220
|
+
|
|
221
|
+
local = (group.ref || group.name).to_s.split(":").last
|
|
222
|
+
declaration = repository.resolve_group(local)
|
|
223
|
+
return [] unless declaration
|
|
224
|
+
|
|
225
|
+
container = Array(declaration.sequence).first ||
|
|
226
|
+
Array(declaration.choice).first || Array(declaration.all).first
|
|
227
|
+
container ? [container] : []
|
|
228
|
+
end
|
|
229
|
+
|
|
213
230
|
# Helper method to create validation errors
|
|
214
231
|
#
|
|
215
232
|
# @param collector [ResultCollector] The result collector
|
data/lib/lutaml/xsd/version.rb
CHANGED